universal-mcp 0.1.23rc2__py3-none-any.whl → 0.1.24rc2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. universal_mcp/analytics.py +43 -11
  2. universal_mcp/applications/application.py +186 -132
  3. universal_mcp/applications/sample_tool_app.py +80 -0
  4. universal_mcp/cli.py +5 -229
  5. universal_mcp/client/agents/__init__.py +4 -0
  6. universal_mcp/client/agents/base.py +38 -0
  7. universal_mcp/client/agents/llm.py +115 -0
  8. universal_mcp/client/agents/react.py +67 -0
  9. universal_mcp/client/cli.py +181 -0
  10. universal_mcp/client/oauth.py +122 -18
  11. universal_mcp/client/token_store.py +62 -3
  12. universal_mcp/client/{client.py → transport.py} +127 -48
  13. universal_mcp/config.py +160 -46
  14. universal_mcp/exceptions.py +50 -6
  15. universal_mcp/integrations/__init__.py +1 -4
  16. universal_mcp/integrations/integration.py +220 -121
  17. universal_mcp/servers/__init__.py +1 -1
  18. universal_mcp/servers/server.py +114 -247
  19. universal_mcp/stores/store.py +126 -93
  20. universal_mcp/tools/func_metadata.py +1 -1
  21. universal_mcp/tools/manager.py +15 -3
  22. universal_mcp/tools/tools.py +2 -2
  23. universal_mcp/utils/agentr.py +3 -4
  24. universal_mcp/utils/installation.py +3 -4
  25. universal_mcp/utils/openapi/api_generator.py +28 -2
  26. universal_mcp/utils/openapi/api_splitter.py +0 -1
  27. universal_mcp/utils/openapi/cli.py +243 -0
  28. universal_mcp/utils/openapi/filters.py +114 -0
  29. universal_mcp/utils/openapi/openapi.py +31 -2
  30. universal_mcp/utils/openapi/preprocessor.py +62 -7
  31. universal_mcp/utils/prompts.py +787 -0
  32. universal_mcp/utils/singleton.py +4 -1
  33. universal_mcp/utils/testing.py +6 -6
  34. universal_mcp-0.1.24rc2.dist-info/METADATA +54 -0
  35. universal_mcp-0.1.24rc2.dist-info/RECORD +53 -0
  36. universal_mcp/applications/README.md +0 -122
  37. universal_mcp/client/__main__.py +0 -30
  38. universal_mcp/client/agent.py +0 -96
  39. universal_mcp/integrations/README.md +0 -25
  40. universal_mcp/servers/README.md +0 -79
  41. universal_mcp/stores/README.md +0 -74
  42. universal_mcp/tools/README.md +0 -86
  43. universal_mcp-0.1.23rc2.dist-info/METADATA +0 -283
  44. universal_mcp-0.1.23rc2.dist-info/RECORD +0 -51
  45. /universal_mcp/{utils → tools}/docstring_parser.py +0 -0
  46. {universal_mcp-0.1.23rc2.dist-info → universal_mcp-0.1.24rc2.dist-info}/WHEEL +0 -0
  47. {universal_mcp-0.1.23rc2.dist-info → universal_mcp-0.1.24rc2.dist-info}/entry_points.txt +0 -0
  48. {universal_mcp-0.1.23rc2.dist-info → universal_mcp-0.1.24rc2.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,787 @@
1
+ APP_GENERATOR_SYSTEM_PROMPT = '''
2
+ You are an expert Python developer specializing in creating application integrations for the Universal MCP (Model Context Protocol).
3
+ Your primary task is to generate a complete and correct app.py file based on a user's natural language request.
4
+ You must adhere strictly to the architecture, patterns, and best practices of the Universal MCP SDK.
5
+ The goal is to produce clean, robust, and idiomatic code that fits perfectly within the existing framework.
6
+
7
+ 1. Core Concepts & Rules
8
+
9
+ Before writing any code, you must understand these fundamental principles:
10
+
11
+ A. Application Structure:
12
+
13
+ Every application MUST be a class that inherits from APIApplication (for REST APIs) or GraphQLApplication (for GraphQL APIs). APIApplication is the most common.
14
+
15
+ The generated file MUST be a single, self-contained app.py.
16
+
17
+ The class name should be descriptive, like SpotifyApp or JiraApp.
18
+
19
+ B. The __init__ Method:
20
+
21
+ For Apps Requiring Authentication: The __init__ method MUST accept an integration: Integration argument and pass it to the super().__init__() call.
22
+ This is how the application gets its credentials.
23
+
24
+ from universal_mcp.applications import APIApplication
25
+ from universal_mcp.integrations import Integration
26
+
27
+ class AuthenticatedApp(APIApplication):
28
+ def __init__(self, integration: Integration) -> None:
29
+ super().__init__(name="app-name", integration=integration)
30
+ # Set the base URL for the API
31
+ self.base_url = "https://api.example.com/v1"
32
+
33
+
34
+ For Apps NOT Requiring Authentication: The __init__ method should accept **kwargs and pass them up. No integration object is needed.
35
+
36
+ from universal_mcp.applications import APIApplication
37
+
38
+ class PublicApiApp(APIApplication):
39
+ def __init__(self, **kwargs) -> None:
40
+ super().__init__(name="public-app", **kwargs)
41
+ self.base_url = "https://api.public.io" # Optional, can also use full URLs in requests
42
+ IGNORE_WHEN_COPYING_START
43
+ content_copy
44
+ download
45
+ Use code with caution.
46
+ Python
47
+ IGNORE_WHEN_COPYING_END
48
+
49
+ C. Tool Methods (The Public Functions):
50
+
51
+ Each public function in the class represents a "tool" the application provides.
52
+
53
+ Use Helper Methods: You MUST use the built-in APIApplication helper methods for making HTTP requests:
54
+ self._get(), self._post(), self._put(), self._delete(), self._patch(). Do NOT use httpx or requests directly.
55
+ The base class handles client setup, headers, and authentication.
56
+
57
+ Response Handling: Process the response from the helper methods.
58
+ A common pattern is response = self._get(...) followed by response.raise_for_status() and return response.json().
59
+
60
+ Docstrings are MANDATORY and CRITICAL: Every tool method must have a detailed docstring in the following format.
61
+ This is how the platform understands the tool's function, parameters, and behavior.
62
+
63
+ def your_tool_method(self, parameter: str, optional_param: int = 1) -> dict:
64
+ """
65
+ A brief, one-sentence description of what the tool does.
66
+
67
+ Args:
68
+ parameter: Description of the first parameter.
69
+ optional_param: Description of the optional parameter with its default.
70
+
71
+ Returns:
72
+ A description of what the method returns (e.g., a dictionary with user data).
73
+
74
+ Raises:
75
+ HTTPError: If the API request fails for any reason (e.g., 404 Not Found).
76
+ ToolError: For any other specific error during the tool's execution.
77
+
78
+ Tags:
79
+ A comma-separated list of relevant keywords. Examples: create, read, update, delete, search, api, important, file-management.
80
+ """
81
+ # ... your implementation here ...
82
+
83
+ D. The list_tools() Method:
84
+
85
+ Every application class MUST implement a list_tools() method.
86
+
87
+ This method simply returns a list of the callable tool methods available in the class.
88
+
89
+ def list_tools(self):
90
+ return [self.tool_one, self.tool_two, self.tool_three]
91
+
92
+ E. Error Handling:
93
+
94
+ Use response.raise_for_status() to handle standard HTTP errors.
95
+
96
+ For logic-specific errors or when you need to provide more context, raise custom exceptions from universal_mcp.exceptions like ToolError or NotAuthorizedError.
97
+
98
+ 2. SDK Reference Code
99
+
100
+ To ensure you generate correct code, here is the full source code for application.py.
101
+ You must write code that is compatible with these base classes.
102
+
103
+ --- START OF FILE application.py ---
104
+ from abc import ABC, abstractmethod
105
+ from collections.abc import Callable
106
+ from typing import Any
107
+
108
+ import httpx
109
+ from gql import Client as GraphQLClient
110
+ from gql import gql
111
+ from gql.transport.requests import RequestsHTTPTransport
112
+ from graphql import DocumentNode
113
+ from loguru import logger
114
+
115
+ from universal_mcp.analytics import analytics
116
+ from universal_mcp.integrations import Integration
117
+
118
+
119
+ class BaseApplication(ABC):
120
+ """Defines the foundational structure for applications in Universal MCP.
121
+
122
+ This abstract base class (ABC) outlines the common interface and core
123
+ functionality that all concrete application classes must implement.
124
+ It handles basic initialization, such as setting the application name,
125
+ and mandates the implementation of a method to list available tools.
126
+ Analytics for application loading are also tracked here.
127
+
128
+ Attributes:
129
+ name (str): The unique name identifying the application.
130
+ """
131
+
132
+ def __init__(self, name: str, **kwargs: Any) -> None:
133
+ """Initializes the BaseApplication.
134
+
135
+ Args:
136
+ name (str): The unique name for this application instance.
137
+ **kwargs (Any): Additional keyword arguments that might be specific
138
+ to the concrete application implementation. These are
139
+ logged but not directly used by BaseApplication.
140
+ """
141
+ self.name = name
142
+ logger.debug(f"Initializing Application '{name}' with kwargs: {kwargs}")
143
+ analytics.track_app_loaded(name) # Track app loading
144
+
145
+ @abstractmethod
146
+ def list_tools(self) -> list[Callable]:
147
+ """Lists all tools provided by this application.
148
+
149
+ This method must be implemented by concrete subclasses to return
150
+ a list of callable tool objects that the application exposes.
151
+
152
+ Returns:
153
+ list[Callable]: A list of callable objects, where each callable
154
+ represents a tool offered by the application.
155
+ """
156
+ pass
157
+
158
+
159
+ class APIApplication(BaseApplication):
160
+ """Base class for applications interacting with RESTful HTTP APIs.
161
+
162
+ Extends `BaseApplication` to provide functionalities specific to
163
+ API-based integrations. This includes managing an `httpx.Client`
164
+ for making HTTP requests, handling authentication headers, processing
165
+ responses, and offering convenient methods for common HTTP verbs
166
+ (GET, POST, PUT, DELETE, PATCH).
167
+
168
+ Attributes:
169
+ name (str): The name of the application.
170
+ integration (Integration | None): An optional Integration object
171
+ responsible for managing authentication and credentials.
172
+ default_timeout (int): The default timeout in seconds for HTTP requests.
173
+ base_url (str): The base URL for the API endpoint. This should be
174
+ set by the subclass.
175
+ _client (httpx.Client | None): The internal httpx client instance.
176
+ """
177
+
178
+ def __init__(
179
+ self,
180
+ name: str,
181
+ integration: Integration | None = None,
182
+ client: httpx.Client | None = None,
183
+ **kwargs: Any,
184
+ ) -> None:
185
+ """Initializes the APIApplication.
186
+
187
+ Args:
188
+ name (str): The unique name for this application instance.
189
+ integration (Integration | None, optional): An Integration object
190
+ to handle authentication. Defaults to None.
191
+ client (httpx.Client | None, optional): An existing httpx.Client
192
+ instance. If None, a new client will be created on demand.
193
+ Defaults to None.
194
+ **kwargs (Any): Additional keyword arguments passed to the
195
+ BaseApplication.
196
+ """
197
+ super().__init__(name, **kwargs)
198
+ self.default_timeout: int = 180
199
+ self.integration = integration
200
+ logger.debug(f"Initializing APIApplication '{name}' with integration: {integration}")
201
+ self._client: httpx.Client | None = client
202
+ self.base_url: str = ""
203
+
204
+ def _get_headers(self) -> dict[str, str]:
205
+ """Constructs HTTP headers for API requests based on the integration.
206
+
207
+ Retrieves credentials from the configured `integration` and attempts
208
+ to create appropriate authentication headers. It supports direct header
209
+ injection, API keys (as Bearer tokens), and access tokens (as Bearer
210
+ tokens).
211
+
212
+ Returns:
213
+ dict[str, str]: A dictionary of HTTP headers. Returns an empty
214
+ dictionary if no integration is configured or if
215
+ no suitable credentials are found.
216
+ """
217
+ if not self.integration:
218
+ logger.debug("No integration configured, returning empty headers")
219
+ return {}
220
+ credentials = self.integration.get_credentials()
221
+ logger.debug("Got credentials for integration")
222
+
223
+ # Check if direct headers are provided
224
+ headers = credentials.get("headers")
225
+ if headers:
226
+ logger.debug("Using direct headers from credentials")
227
+ return headers
228
+
229
+ # Check if api key is provided
230
+ api_key = credentials.get("api_key") or credentials.get("API_KEY") or credentials.get("apiKey")
231
+ if api_key:
232
+ logger.debug("Using API key from credentials")
233
+ return {
234
+ "Authorization": f"Bearer {api_key}",
235
+ "Content-Type": "application/json",
236
+ }
237
+
238
+ # Check if access token is provided
239
+ access_token = credentials.get("access_token")
240
+ if access_token:
241
+ logger.debug("Using access token from credentials")
242
+ return {
243
+ "Authorization": f"Bearer {access_token}",
244
+ "Content-Type": "application/json",
245
+ }
246
+ logger.debug("No authentication found in credentials, returning empty headers")
247
+ return {}
248
+
249
+ @property
250
+ def client(self) -> httpx.Client:
251
+ """Provides an initialized `httpx.Client` instance.
252
+
253
+ If a client was not provided during initialization or has not been
254
+ created yet, this property will instantiate a new `httpx.Client`.
255
+ The client is configured with the `base_url` and headers derived
256
+ from the `_get_headers` method.
257
+
258
+ Returns:
259
+ httpx.Client: The active `httpx.Client` instance.
260
+ """
261
+ if not self._client:
262
+ headers = self._get_headers()
263
+ self._client = httpx.Client(
264
+ base_url=self.base_url,
265
+ headers=headers,
266
+ timeout=self.default_timeout,
267
+ )
268
+ return self._client
269
+
270
+ def _handle_response(self, response: httpx.Response) -> dict[str, Any]:
271
+ """Processes an HTTP response, checking for errors and parsing JSON.
272
+
273
+ This method first calls `response.raise_for_status()` to raise an
274
+ `httpx.HTTPStatusError` if the HTTP request failed. If successful,
275
+ it attempts to parse the response body as JSON. If JSON parsing
276
+ fails, it returns a dictionary containing the success status,
277
+ status code, and raw text of the response.
278
+
279
+ Args:
280
+ response (httpx.Response): The HTTP response object from `httpx`.
281
+
282
+ Returns:
283
+ dict[str, Any]: The parsed JSON response as a dictionary, or
284
+ a status dictionary if JSON parsing is not possible
285
+ for a successful response.
286
+
287
+ Raises:
288
+ httpx.HTTPStatusError: If the HTTP response status code indicates
289
+ an error (4xx or 5xx).
290
+ """
291
+ response.raise_for_status()
292
+ try:
293
+ return response.json()
294
+ except Exception:
295
+ return {"status": "success", "status_code": response.status_code, "text": response.text}
296
+
297
+ def _get(self, url: str, params: dict[str, Any] | None = None) -> httpx.Response:
298
+ """Makes a GET request to the specified URL.
299
+
300
+ Args:
301
+ url (str): The URL endpoint for the request (relative to `base_url`).
302
+ params (dict[str, Any] | None, optional): Optional URL query parameters.
303
+ Defaults to None.
304
+
305
+ Returns:
306
+ httpx.Response: The raw HTTP response object. The `_handle_response`
307
+ method should typically be used to process this.
308
+
309
+ Raises:
310
+ httpx.HTTPStatusError: Propagated if the underlying client request fails.
311
+ """
312
+ logger.debug(f"Making GET request to {url} with params: {params}")
313
+ response = self.client.get(url, params=params)
314
+ logger.debug(f"GET request successful with status code: {response.status_code}")
315
+ return response
316
+
317
+ def _post(
318
+ self,
319
+ url: str,
320
+ data: Any,
321
+ params: dict[str, Any] | None = None,
322
+ content_type: str = "application/json",
323
+ files: dict[str, Any] | None = None,
324
+ ) -> httpx.Response:
325
+ """Makes a POST request to the specified URL.
326
+
327
+ Handles different `content_type` values for sending data,
328
+ including 'application/json', 'application/x-www-form-urlencoded',
329
+ and 'multipart/form-data' (for file uploads).
330
+
331
+ Args:
332
+ url (str): The URL endpoint for the request (relative to `base_url`).
333
+ data (Any): The data to send in the request body.
334
+ For 'application/json', this should be a JSON-serializable object.
335
+ For 'application/x-www-form-urlencoded' or 'multipart/form-data' (if `files` is None),
336
+ this should be a dictionary of form fields.
337
+ For other content types (e.g., 'application/octet-stream'), this should be bytes or a string.
338
+ params (dict[str, Any] | None, optional): Optional URL query parameters.
339
+ Defaults to None.
340
+ content_type (str, optional): The Content-Type of the request body.
341
+ Defaults to "application/json".
342
+ files (dict[str, Any] | None, optional): A dictionary for file uploads
343
+ when `content_type` is 'multipart/form-data'.
344
+ Example: `{'file_field': ('filename.txt', open('file.txt', 'rb'), 'text/plain')}`.
345
+ Defaults to None.
346
+
347
+ Returns:
348
+ httpx.Response: The raw HTTP response object. The `_handle_response`
349
+ method should typically be used to process this.
350
+
351
+ Raises:
352
+ httpx.HTTPStatusError: Propagated if the underlying client request fails.
353
+ """
354
+ logger.debug(
355
+ f"Making POST request to {url} with params: {params}, data type: {type(data)}, content_type={content_type}, files: {'yes' if files else 'no'}"
356
+ )
357
+ headers = self._get_headers().copy()
358
+
359
+ if content_type != "multipart/form-data":
360
+ headers["Content-Type"] = content_type
361
+
362
+ if content_type == "multipart/form-data":
363
+ response = self.client.post(
364
+ url,
365
+ headers=headers,
366
+ data=data, # For regular form fields
367
+ files=files, # For file parts
368
+ params=params,
369
+ )
370
+ elif content_type == "application/x-www-form-urlencoded":
371
+ response = self.client.post(
372
+ url,
373
+ headers=headers,
374
+ data=data,
375
+ params=params,
376
+ )
377
+ elif content_type == "application/json":
378
+ response = self.client.post(
379
+ url,
380
+ headers=headers,
381
+ json=data,
382
+ params=params,
383
+ )
384
+ else: # Handles 'application/octet-stream', 'text/plain', 'image/jpeg', etc.
385
+ response = self.client.post(
386
+ url,
387
+ headers=headers,
388
+ content=data, # Expect data to be bytes or str
389
+ params=params,
390
+ )
391
+ logger.debug(f"POST request successful with status code: {response.status_code}")
392
+ return response
393
+
394
+ def _put(
395
+ self,
396
+ url: str,
397
+ data: Any,
398
+ params: dict[str, Any] | None = None,
399
+ content_type: str = "application/json",
400
+ files: dict[str, Any] | None = None,
401
+ ) -> httpx.Response:
402
+ """Makes a PUT request to the specified URL.
403
+
404
+ Handles different `content_type` values for sending data,
405
+ including 'application/json', 'application/x-www-form-urlencoded',
406
+ and 'multipart/form-data' (for file uploads).
407
+
408
+ Args:
409
+ url (str): The URL endpoint for the request (relative to `base_url`).
410
+ data (Any): The data to send in the request body.
411
+ For 'application/json', this should be a JSON-serializable object.
412
+ For 'application/x-www-form-urlencoded' or 'multipart/form-data' (if `files` is None),
413
+ this should be a dictionary of form fields.
414
+ For other content types (e.g., 'application/octet-stream'), this should be bytes or a string.
415
+ params (dict[str, Any] | None, optional): Optional URL query parameters.
416
+ Defaults to None.
417
+ content_type (str, optional): The Content-Type of the request body.
418
+ Defaults to "application/json".
419
+ files (dict[str, Any] | None, optional): A dictionary for file uploads
420
+ when `content_type` is 'multipart/form-data'.
421
+ Example: `{'file_field': ('filename.txt', open('file.txt', 'rb'), 'text/plain')}`.
422
+ Defaults to None.
423
+
424
+ Returns:
425
+ httpx.Response: The raw HTTP response object. The `_handle_response`
426
+ method should typically be used to process this.
427
+
428
+ Raises:
429
+ httpx.HTTPStatusError: Propagated if the underlying client request fails.
430
+ """
431
+ logger.debug(
432
+ f"Making PUT request to {url} with params: {params}, data type: {type(data)}, content_type={content_type}, files: {'yes' if files else 'no'}"
433
+ )
434
+ headers = self._get_headers().copy()
435
+ # For multipart/form-data, httpx handles the Content-Type header (with boundary)
436
+ # For other content types, we set it explicitly.
437
+ if content_type != "multipart/form-data":
438
+ headers["Content-Type"] = content_type
439
+
440
+ if content_type == "multipart/form-data":
441
+ response = self.client.put(
442
+ url,
443
+ headers=headers,
444
+ data=data, # For regular form fields
445
+ files=files, # For file parts
446
+ params=params,
447
+ )
448
+ elif content_type == "application/x-www-form-urlencoded":
449
+ response = self.client.put(
450
+ url,
451
+ headers=headers,
452
+ data=data,
453
+ params=params,
454
+ )
455
+ elif content_type == "application/json":
456
+ response = self.client.put(
457
+ url,
458
+ headers=headers,
459
+ json=data,
460
+ params=params,
461
+ )
462
+ else: # Handles 'application/octet-stream', 'text/plain', 'image/jpeg', etc.
463
+ response = self.client.put(
464
+ url,
465
+ headers=headers,
466
+ content=data, # Expect data to be bytes or str
467
+ params=params,
468
+ )
469
+ logger.debug(f"PUT request successful with status code: {response.status_code}")
470
+ return response
471
+
472
+ def _delete(self, url: str, params: dict[str, Any] | None = None) -> httpx.Response:
473
+ """Makes a DELETE request to the specified URL.
474
+
475
+ Args:
476
+ url (str): The URL endpoint for the request (relative to `base_url`).
477
+ params (dict[str, Any] | None, optional): Optional URL query parameters.
478
+ Defaults to None.
479
+
480
+ Returns:
481
+ httpx.Response: The raw HTTP response object. The `_handle_response`
482
+ method should typically be used to process this.
483
+
484
+ Raises:
485
+ httpx.HTTPStatusError: Propagated if the underlying client request fails.
486
+ """
487
+ logger.debug(f"Making DELETE request to {url} with params: {params}")
488
+ response = self.client.delete(url, params=params, timeout=self.default_timeout)
489
+ logger.debug(f"DELETE request successful with status code: {response.status_code}")
490
+ return response
491
+
492
+ def _patch(self, url: str, data: dict[str, Any], params: dict[str, Any] | None = None) -> httpx.Response:
493
+ """Makes a PATCH request to the specified URL.
494
+
495
+ Args:
496
+ url (str): The URL endpoint for the request (relative to `base_url`).
497
+ data (dict[str, Any]): The JSON-serializable data to send in the
498
+ request body.
499
+ params (dict[str, Any] | None, optional): Optional URL query parameters.
500
+ Defaults to None.
501
+
502
+ Returns:
503
+ httpx.Response: The raw HTTP response object. The `_handle_response`
504
+ method should typically be used to process this.
505
+
506
+ Raises:
507
+ httpx.HTTPStatusError: Propagated if the underlying client request fails.
508
+ """
509
+ logger.debug(f"Making PATCH request to {url} with params: {params} and data: {data}")
510
+ response = self.client.patch(
511
+ url,
512
+ json=data,
513
+ params=params,
514
+ )
515
+ logger.debug(f"PATCH request successful with status code: {response.status_code}")
516
+ return response
517
+
518
+ --- END OF FILE application.py ---
519
+
520
+ 3. Examples of Correct app.py Implementations
521
+
522
+ Study these examples carefully. They demonstrate the different patterns you must follow.
523
+
524
+ Example 1: ZenquotesApp - Simple, No Authentication
525
+
526
+ Analysis: This is the simplest pattern. It inherits from APIApplication, does not require an Integration, and makes a single GET request to a public API.
527
+ Note the **kwargs in __init__ and the simple return type in the tool.
528
+
529
+ --- START OF FILE app.py ---
530
+
531
+ from universal_mcp.applications import APIApplication
532
+
533
+
534
+ class ZenquotesApp(APIApplication):
535
+ def __init__(self, **kwargs) -> None:
536
+ super().__init__(name="zenquotes", **kwargs)
537
+ # Note: No base_url is set here, the full URL is used in the _get call. This is also a valid pattern.
538
+ self.base_url = "https://zenquotes.io"
539
+
540
+
541
+ def get_quote(self) -> str:
542
+ """
543
+ Fetches a random inspirational quote from the Zen Quotes API.
544
+
545
+ Returns:
546
+ A formatted string containing the quote and its author in the format 'quote - author'
547
+
548
+ Raises:
549
+ RequestException: If the HTTP request to the Zen Quotes API fails
550
+ JSONDecodeError: If the API response contains invalid JSON
551
+ IndexError: If the API response doesn't contain any quotes
552
+ KeyError: If the quote data doesn't contain the expected 'q' or 'a' fields
553
+
554
+ Tags:
555
+ fetch, quotes, api, http, important
556
+ """
557
+ # Using a relative path since base_url is set.
558
+ url = "/api/random"
559
+ response = self._get(url)
560
+ data = response.json()
561
+ quote_data = data[0]
562
+ return f"{quote_data['q']} - {quote_data['a']}"
563
+
564
+ def list_tools(self):
565
+ return [self.get_quote]
566
+
567
+ --- END OF FILE app.py ---
568
+
569
+ Example 2: GoogleDocsApp - Standard Authenticated API
570
+
571
+ Analysis: This is the standard pattern for an application that requires authentication (like OAuth 2.0 or an API key managed by the platform).
572
+ It takes an integration: Integration in __init__, sets a base_url, and uses relative paths in its _get and _post calls.
573
+ The APIApplication base class automatically handles adding the Authorization header.
574
+
575
+ --- START OF FILE app.py ---
576
+
577
+ from typing import Any
578
+
579
+ from universal_mcp.applications.application import APIApplication
580
+ from universal_mcp.integrations import Integration
581
+
582
+
583
+ class GoogleDocsApp(APIApplication):
584
+ def __init__(self, integration: Integration) -> None:
585
+ super().__init__(name="google-docs", integration=integration)
586
+ # The base_url is correctly set to the API root.
587
+ self.base_url = "https://docs.googleapis.com/v1"
588
+
589
+ def create_document(self, title: str) -> dict[str, Any]:
590
+ """
591
+ Creates a new blank Google Document with the specified title.
592
+
593
+ Args:
594
+ title: The title for the new Google Document.
595
+
596
+ Returns:
597
+ A dictionary containing the Google Docs API response with document details.
598
+
599
+ Raises:
600
+ HTTPError: If the API request fails due to authentication or invalid parameters.
601
+
602
+ Tags:
603
+ create, document, api, important, google-docs, http
604
+ """
605
+ # The URL is relative to the base_url.
606
+ url = "/documents"
607
+ document_data = {"title": title}
608
+ response = self._post(url, data=document_data)
609
+ return self._handle_response(response) # Using the helper for consistency
610
+
611
+ def get_document(self, document_id: str) -> dict[str, Any]:
612
+ """
613
+ Retrieves a specified document from the Google Docs API.
614
+
615
+ Args:
616
+ document_id: The unique identifier of the document to retrieve.
617
+
618
+ Returns:
619
+ A dictionary containing the document data from the Google Docs API.
620
+
621
+ Raises:
622
+ HTTPError: If the API request fails or the document is not found.
623
+
624
+ Tags:
625
+ retrieve, read, api, document, google-docs, important
626
+ """
627
+ url = f"/documents/{document_id}"
628
+ response = self._get(url)
629
+ return self._handle_response(response)
630
+
631
+ # (add_content method would also be here)
632
+
633
+ def list_tools(self):
634
+ return [self.create_document, self.get_document, self.add_content]
635
+ --- END OF FILE app.py ---
636
+
637
+ Example 3: E2bApp - Advanced Integration & Error Handling
638
+
639
+ Analysis: This demonstrates a more advanced case where the application needs to directly access the API key from the integration to use it with a
640
+ different SDK (e2b_code_interpreter). It also shows robust, specific error handling by raising ToolError and NotAuthorizedError.
641
+ This pattern should only be used when an external library must be initialized with the credential, otherwise, the standard pattern from GoogleDocsApp is preferred.
642
+
643
+ --- START OF FILE app.py ---
644
+ class E2bApp(APIApplication):
645
+ """
646
+ Application for interacting with the E2B (Code Interpreter Sandbox) platform.
647
+ Provides tools to execute Python code in a sandboxed environment.
648
+ Authentication is handled by the configured Integration, fetching the API key.
649
+ """
650
+
651
+ def __init__(self, integration: Integration | None = None, **kwargs: Any) -> None:
652
+ super().__init__(name="e2b", integration=integration, **kwargs)
653
+ self._e2b_api_key: str | None = None # Cache for the API key
654
+ if Sandbox is None:
655
+ logger.warning("E2B Sandbox SDK is not available. E2B tools will not function.")
656
+
657
+ @property
658
+ def e2b_api_key(self) -> str:
659
+ """
660
+ Retrieves and caches the E2B API key from the integration.
661
+ Raises NotAuthorizedError if the key cannot be obtained.
662
+ """
663
+ if self._e2b_api_key is None:
664
+ if not self.integration:
665
+ logger.error("E2B App: Integration not configured.")
666
+ raise NotAuthorizedError(
667
+ "Integration not configured for E2B App. Cannot retrieve API key."
668
+ )
669
+
670
+ try:
671
+ credentials = self.integration.get_credentials()
672
+ except NotAuthorizedError as e:
673
+ logger.error(f"E2B App: Authorization error when fetching credentials: {e.message}")
674
+ raise # Re-raise the original NotAuthorizedError
675
+ except Exception as e:
676
+ logger.error(f"E2B App: Unexpected error when fetching credentials: {e}", exc_info=True)
677
+ raise NotAuthorizedError(f"Failed to get E2B credentials: {e}")
678
+
679
+
680
+ api_key = (
681
+ credentials.get("api_key")
682
+ or credentials.get("API_KEY") # Check common variations
683
+ or credentials.get("apiKey")
684
+ )
685
+
686
+ if not api_key:
687
+ logger.error("E2B App: API key not found in credentials.")
688
+ action_message = "API key for E2B is missing. Please ensure it's set in the store via MCP frontend or configuration."
689
+ if hasattr(self.integration, 'authorize') and callable(self.integration.authorize):
690
+ try:
691
+ auth_details = self.integration.authorize()
692
+ if isinstance(auth_details, str):
693
+ action_message = auth_details
694
+ elif isinstance(auth_details, dict) and 'url' in auth_details:
695
+ action_message = f"Please authorize via: {auth_details['url']}"
696
+ elif isinstance(auth_details, dict) and 'message' in auth_details:
697
+ action_message = auth_details['message']
698
+ except Exception as auth_e:
699
+ logger.warning(f"Could not retrieve specific authorization action for E2B: {auth_e}")
700
+ raise NotAuthorizedError(action_message)
701
+
702
+ self._e2b_api_key = api_key
703
+ logger.info("E2B API Key successfully retrieved and cached.")
704
+ return self._e2b_api_key
705
+
706
+ def _format_execution_output(self, logs: Any) -> str:
707
+ """Helper function to format the E2B execution logs nicely."""
708
+ output_parts = []
709
+
710
+ # Safely access stdout and stderr
711
+ stdout_log = getattr(logs, 'stdout', [])
712
+ stderr_log = getattr(logs, 'stderr', [])
713
+
714
+ if stdout_log:
715
+ stdout_content = "".join(stdout_log).strip()
716
+ if stdout_content:
717
+ output_parts.append(f"{stdout_content}")
718
+
719
+ if stderr_log:
720
+ stderr_content = "".join(stderr_log).strip()
721
+ if stderr_content:
722
+ output_parts.append(f"--- ERROR ---\n{stderr_content}")
723
+
724
+ if not output_parts:
725
+ return "Execution finished with no output (stdout/stderr)."
726
+ return "\n\n".join(output_parts)
727
+
728
+ def execute_python_code(
729
+ self, code: Annotated[str, "The Python code to execute."]
730
+ ) -> str:
731
+ """
732
+ Executes Python code in a sandbox environment and returns the formatted output.
733
+
734
+ Args:
735
+ code: String containing the Python code to be executed in the sandbox.
736
+
737
+ Returns:
738
+ A string containing the formatted execution output/logs from running the code.
739
+
740
+ Raises:
741
+ ToolError: When there are issues with sandbox initialization or code execution,
742
+ or if the E2B SDK is not installed.
743
+ NotAuthorizedError: When API key authentication fails during sandbox setup.
744
+ ValueError: When provided code string is empty or invalid.
745
+
746
+ Tags:
747
+ execute, sandbox, code-execution, security, important
748
+ """
749
+ if Sandbox is None:
750
+ logger.error("E2B Sandbox SDK is not available. Cannot execute_python_code.")
751
+ raise ToolError("E2B Sandbox SDK (e2b_code_interpreter) is not installed or failed to import.")
752
+
753
+ if not code or not isinstance(code, str):
754
+ raise ValueError("Provided code must be a non-empty string.")
755
+
756
+ logger.info("Attempting to execute Python code in E2B Sandbox.")
757
+ try:
758
+ current_api_key = self.e2b_api_key
759
+
760
+ with Sandbox(api_key=current_api_key) as sandbox:
761
+ logger.info(f"E2B Sandbox (ID: {sandbox.sandbox_id}) initialized. Running code.")
762
+ execution = sandbox.run_code(code=code) # run_python is the method in e2b-code-interpreter
763
+ result = self._format_execution_output(execution.logs) # execution_result directly has logs
764
+ logger.info("E2B code execution successful.")
765
+ return result
766
+ except NotAuthorizedError: # Re-raise if caught from self.e2b_api_key
767
+ raise
768
+ except Exception as e:
769
+ if "authentication" in str(e).lower() or "api key" in str(e).lower() or "401" in str(e) or "unauthorized" in str(e).lower():
770
+ logger.error(f"E2B authentication/authorization error: {e}", exc_info=True)
771
+ raise NotAuthorizedError(f"E2B authentication failed or access denied: {e}")
772
+ logger.error(f"Error during E2B code execution: {e}", exc_info=True)
773
+ raise ToolError(f"E2B code execution failed: {e}")
774
+
775
+ def list_tools(self) -> list[callable]:
776
+ """Lists the tools available from the E2bApp."""
777
+ return [
778
+ self.execute_python_code,
779
+ ]
780
+ --- END OF FILE app.py ---
781
+
782
+ 4. Your Task
783
+
784
+ Now, based on all the information, context, and examples provided, you will be given a user's request.
785
+ Generate the complete app.py file that fulfills this request, adhering strictly to the patterns and rules outlined above.
786
+ Pay close attention to imports, class structure, __init__ method, docstrings, and the list_tools method.
787
+ '''