universal-mcp 0.1.13rc14__py3-none-any.whl → 0.1.15__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/analytics.py +7 -1
- universal_mcp/applications/README.md +122 -0
- universal_mcp/applications/__init__.py +48 -46
- universal_mcp/applications/application.py +249 -40
- universal_mcp/cli.py +49 -49
- universal_mcp/config.py +95 -22
- universal_mcp/exceptions.py +8 -0
- universal_mcp/integrations/integration.py +18 -2
- universal_mcp/logger.py +59 -8
- universal_mcp/servers/__init__.py +2 -2
- universal_mcp/stores/store.py +2 -12
- universal_mcp/tools/__init__.py +14 -2
- universal_mcp/tools/adapters.py +25 -0
- universal_mcp/tools/func_metadata.py +12 -2
- universal_mcp/tools/manager.py +236 -0
- universal_mcp/tools/tools.py +5 -249
- universal_mcp/utils/common.py +33 -0
- universal_mcp/utils/installation.py +8 -8
- universal_mcp/utils/openapi/__inti__.py +0 -0
- universal_mcp/utils/{api_generator.py → openapi/api_generator.py} +1 -1
- universal_mcp/utils/openapi/openapi.py +930 -0
- universal_mcp/utils/openapi/preprocessor.py +1223 -0
- universal_mcp/utils/{readme.py → openapi/readme.py} +21 -31
- universal_mcp/utils/templates/README.md.j2 +17 -0
- {universal_mcp-0.1.13rc14.dist-info → universal_mcp-0.1.15.dist-info}/METADATA +6 -3
- universal_mcp-0.1.15.dist-info/RECORD +44 -0
- universal_mcp-0.1.15.dist-info/licenses/LICENSE +21 -0
- universal_mcp/templates/README.md.j2 +0 -93
- universal_mcp/utils/dump_app_tools.py +0 -78
- universal_mcp/utils/openapi.py +0 -697
- universal_mcp-0.1.13rc14.dist-info/RECORD +0 -39
- /universal_mcp/utils/{docgen.py → openapi/docgen.py} +0 -0
- /universal_mcp/{templates → utils/templates}/api_client.py.j2 +0 -0
- {universal_mcp-0.1.13rc14.dist-info → universal_mcp-0.1.15.dist-info}/WHEEL +0 -0
- {universal_mcp-0.1.13rc14.dist-info → universal_mcp-0.1.15.dist-info}/entry_points.txt +0 -0
universal_mcp/analytics.py
CHANGED
@@ -48,7 +48,12 @@ class Analytics:
|
|
48
48
|
logger.error(f"Failed to track app_loaded event: {e}")
|
49
49
|
|
50
50
|
def track_tool_called(
|
51
|
-
self,
|
51
|
+
self,
|
52
|
+
tool_name: str,
|
53
|
+
app_name: str,
|
54
|
+
status: str,
|
55
|
+
error: str = None,
|
56
|
+
user_id=None,
|
52
57
|
):
|
53
58
|
"""Track when a tool is called
|
54
59
|
|
@@ -63,6 +68,7 @@ class Analytics:
|
|
63
68
|
try:
|
64
69
|
properties = {
|
65
70
|
"tool_name": tool_name,
|
71
|
+
"app_name": app_name,
|
66
72
|
"status": status,
|
67
73
|
"error": error,
|
68
74
|
"version": self.get_version(),
|
@@ -0,0 +1,122 @@
|
|
1
|
+
# Universal MCP Applications Module
|
2
|
+
|
3
|
+
This module provides the core functionality for managing and integrating applications within the Universal MCP system. It offers a flexible framework for creating, managing, and interacting with various types of applications through a unified interface.
|
4
|
+
|
5
|
+
## Overview
|
6
|
+
|
7
|
+
The applications module provides three main base classes for building application integrations:
|
8
|
+
|
9
|
+
1. `BaseApplication`: The abstract base class that defines the common interface for all applications
|
10
|
+
2. `APIApplication`: A concrete implementation for applications that communicate via HTTP APIs
|
11
|
+
3. `GraphQLApplication`: A specialized implementation for applications that use GraphQL APIs
|
12
|
+
|
13
|
+
## Key Features
|
14
|
+
|
15
|
+
- **Dynamic Application Loading**: Applications can be loaded dynamically from external packages
|
16
|
+
- **Unified Credential Management**: Centralized handling of application credentials
|
17
|
+
- **HTTP API Support**: Built-in support for RESTful API interactions
|
18
|
+
- **GraphQL Support**: Specialized support for GraphQL-based applications
|
19
|
+
- **Automatic Package Installation**: Automatic installation of application packages from GitHub
|
20
|
+
|
21
|
+
## Base Classes
|
22
|
+
|
23
|
+
### BaseApplication
|
24
|
+
|
25
|
+
The foundation class for all applications, providing:
|
26
|
+
- Basic initialization
|
27
|
+
- Credential management
|
28
|
+
- Tool listing interface
|
29
|
+
|
30
|
+
### APIApplication
|
31
|
+
|
32
|
+
Extends BaseApplication to provide:
|
33
|
+
- HTTP client management
|
34
|
+
- Authentication handling
|
35
|
+
- Common HTTP methods (GET, POST, PUT, DELETE, PATCH)
|
36
|
+
- Request/response handling
|
37
|
+
|
38
|
+
### GraphQLApplication
|
39
|
+
|
40
|
+
Specialized for GraphQL-based applications, offering:
|
41
|
+
- GraphQL client management
|
42
|
+
- Query and mutation execution
|
43
|
+
- Authentication handling
|
44
|
+
|
45
|
+
## Usage
|
46
|
+
|
47
|
+
### Creating a New Application
|
48
|
+
|
49
|
+
1. Create a new package following the naming convention: `universal_mcp_<app_name>`
|
50
|
+
2. Implement your application class inheriting from one of the base classes
|
51
|
+
3. Name your class following the convention: `<AppName>App`
|
52
|
+
|
53
|
+
Example:
|
54
|
+
```python
|
55
|
+
from universal_mcp.applications import APIApplication
|
56
|
+
|
57
|
+
class MyApp(APIApplication):
|
58
|
+
def __init__(self, name: str, integration=None, **kwargs):
|
59
|
+
super().__init__(name, integration, **kwargs)
|
60
|
+
self.base_url = "https://api.example.com"
|
61
|
+
|
62
|
+
def list_tools(self):
|
63
|
+
return [self.my_tool]
|
64
|
+
|
65
|
+
def my_tool(self):
|
66
|
+
# Implementation here
|
67
|
+
pass
|
68
|
+
```
|
69
|
+
|
70
|
+
### Loading an Application
|
71
|
+
|
72
|
+
```python
|
73
|
+
from universal_mcp.applications import app_from_slug
|
74
|
+
|
75
|
+
# The system will automatically install the package if needed
|
76
|
+
MyApp = app_from_slug("my-app")
|
77
|
+
app = MyApp("my-app-instance")
|
78
|
+
```
|
79
|
+
|
80
|
+
## Authentication
|
81
|
+
|
82
|
+
The module supports various authentication methods:
|
83
|
+
- API Keys
|
84
|
+
- Access Tokens
|
85
|
+
- Custom Headers
|
86
|
+
- Bearer Tokens
|
87
|
+
|
88
|
+
Credentials are managed through the integration system and can be accessed via the `credentials` property.
|
89
|
+
|
90
|
+
## Error Handling
|
91
|
+
|
92
|
+
The module includes comprehensive error handling for:
|
93
|
+
- Package installation failures
|
94
|
+
- Import errors
|
95
|
+
- API request failures
|
96
|
+
- Authentication issues
|
97
|
+
|
98
|
+
## Logging
|
99
|
+
|
100
|
+
All operations are logged using the `loguru` logger, providing detailed information about:
|
101
|
+
- Application initialization
|
102
|
+
- API requests
|
103
|
+
- Authentication attempts
|
104
|
+
- Package installation
|
105
|
+
- Error conditions
|
106
|
+
|
107
|
+
## Requirements
|
108
|
+
|
109
|
+
- Python 3.8+
|
110
|
+
- httpx
|
111
|
+
- gql
|
112
|
+
- loguru
|
113
|
+
- uv (for package installation)
|
114
|
+
|
115
|
+
## Contributing
|
116
|
+
|
117
|
+
To contribute a new application:
|
118
|
+
1. Create a new package following the naming conventions
|
119
|
+
2. Implement the application class
|
120
|
+
3. Add proper error handling and logging
|
121
|
+
4. Include comprehensive documentation
|
122
|
+
5. Submit a pull request to the Universal MCP repository
|
@@ -2,6 +2,7 @@ import importlib
|
|
2
2
|
import os
|
3
3
|
import subprocess
|
4
4
|
import sys
|
5
|
+
from importlib.metadata import version
|
5
6
|
|
6
7
|
from loguru import logger
|
7
8
|
|
@@ -10,6 +11,12 @@ from universal_mcp.applications.application import (
|
|
10
11
|
BaseApplication,
|
11
12
|
GraphQLApplication,
|
12
13
|
)
|
14
|
+
from universal_mcp.utils.common import (
|
15
|
+
get_default_class_name,
|
16
|
+
get_default_module_path,
|
17
|
+
get_default_package_name,
|
18
|
+
get_default_repository_path,
|
19
|
+
)
|
13
20
|
|
14
21
|
UNIVERSAL_MCP_HOME = os.path.join(os.path.expanduser("~"), ".universal-mcp", "packages")
|
15
22
|
|
@@ -24,41 +31,36 @@ sys.path.append(UNIVERSAL_MCP_HOME)
|
|
24
31
|
# Class name is NameApp, eg, GoogleCalendarApp
|
25
32
|
|
26
33
|
|
27
|
-
def
|
28
|
-
"""
|
29
|
-
Helper to import a class by name from a module.
|
30
|
-
Raises ModuleNotFoundError if module or class does not exist.
|
31
|
-
"""
|
32
|
-
try:
|
33
|
-
module = importlib.import_module(module_path)
|
34
|
-
except ModuleNotFoundError as e:
|
35
|
-
logger.debug(f"Import failed for module '{module_path}': {e}")
|
36
|
-
raise
|
37
|
-
try:
|
38
|
-
return getattr(module, class_name)
|
39
|
-
except AttributeError as e:
|
40
|
-
logger.error(f"Class '{class_name}' not found in module '{module_path}'")
|
41
|
-
raise ModuleNotFoundError(
|
42
|
-
f"Class '{class_name}' not found in module '{module_path}'"
|
43
|
-
) from e
|
44
|
-
|
45
|
-
|
46
|
-
def _install_package(slug_clean: str):
|
34
|
+
def _install_or_upgrade_package(package_name: str, repository_path: str):
|
47
35
|
"""
|
48
36
|
Helper to install a package via pip from the universal-mcp GitHub repository.
|
49
37
|
"""
|
50
|
-
|
51
|
-
|
52
|
-
|
38
|
+
try:
|
39
|
+
current_version = version(package_name)
|
40
|
+
logger.info(f"Current version of {package_name} is {current_version}")
|
41
|
+
except ImportError:
|
42
|
+
current_version = None
|
43
|
+
if current_version is not None:
|
44
|
+
return
|
45
|
+
cmd = [
|
46
|
+
"uv",
|
47
|
+
"pip",
|
48
|
+
"install",
|
49
|
+
"--upgrade",
|
50
|
+
repository_path,
|
51
|
+
"--target",
|
52
|
+
UNIVERSAL_MCP_HOME,
|
53
|
+
]
|
54
|
+
logger.debug(f"Installing package '{package_name}' with command: {' '.join(cmd)}")
|
53
55
|
try:
|
54
56
|
subprocess.check_call(cmd)
|
55
57
|
except subprocess.CalledProcessError as e:
|
56
|
-
logger.error(f"Installation failed for '{
|
58
|
+
logger.error(f"Installation failed for '{package_name}': {e}")
|
57
59
|
raise ModuleNotFoundError(
|
58
|
-
f"Installation failed for package '{
|
60
|
+
f"Installation failed for package '{package_name}'"
|
59
61
|
) from e
|
60
62
|
else:
|
61
|
-
logger.
|
63
|
+
logger.debug(f"Package {package_name} installed successfully")
|
62
64
|
|
63
65
|
|
64
66
|
def app_from_slug(slug: str):
|
@@ -66,29 +68,29 @@ def app_from_slug(slug: str):
|
|
66
68
|
Dynamically resolve and return the application class for the given slug.
|
67
69
|
Attempts installation from GitHub if the package is not found locally.
|
68
70
|
"""
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
logger.info(
|
71
|
+
class_name = get_default_class_name(slug)
|
72
|
+
module_path = get_default_module_path(slug)
|
73
|
+
package_name = get_default_package_name(slug)
|
74
|
+
repository_path = get_default_repository_path(slug)
|
75
|
+
logger.debug(
|
75
76
|
f"Resolving app for slug '{slug}' → module '{module_path}', class '{class_name}'"
|
76
77
|
)
|
77
78
|
try:
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
79
|
+
_install_or_upgrade_package(package_name, repository_path)
|
80
|
+
module = importlib.import_module(module_path)
|
81
|
+
class_ = getattr(module, class_name)
|
82
|
+
logger.debug(f"Loaded class '{class_}' from module '{module_path}'")
|
83
|
+
return class_
|
84
|
+
except ModuleNotFoundError as e:
|
85
|
+
raise ModuleNotFoundError(
|
86
|
+
f"Package '{module_path}' not found locally. Please install it first."
|
87
|
+
) from e
|
88
|
+
except AttributeError as e:
|
89
|
+
raise AttributeError(
|
90
|
+
f"Class '{class_name}' not found in module '{module_path}'"
|
91
|
+
) from e
|
92
|
+
except Exception as e:
|
93
|
+
raise Exception(f"Error importing module '{module_path}': {e}") from e
|
92
94
|
|
93
95
|
|
94
96
|
__all__ = [
|
@@ -1,7 +1,10 @@
|
|
1
1
|
from abc import ABC, abstractmethod
|
2
|
+
from collections.abc import Callable
|
3
|
+
from typing import Any
|
2
4
|
|
3
5
|
import httpx
|
4
|
-
from gql import Client
|
6
|
+
from gql import Client as GraphQLClient
|
7
|
+
from gql import gql
|
5
8
|
from gql.transport.requests import RequestsHTTPTransport
|
6
9
|
from graphql import DocumentNode
|
7
10
|
from loguru import logger
|
@@ -12,41 +15,95 @@ from universal_mcp.integrations import Integration
|
|
12
15
|
|
13
16
|
class BaseApplication(ABC):
|
14
17
|
"""
|
15
|
-
|
18
|
+
Base class for all applications in the Universal MCP system.
|
19
|
+
|
20
|
+
This abstract base class defines the common interface and functionality
|
21
|
+
that all applications must implement. It provides basic initialization
|
22
|
+
and credential management capabilities.
|
23
|
+
|
24
|
+
Attributes:
|
25
|
+
name (str): The name of the application
|
26
|
+
_credentials (Optional[Dict[str, Any]]): Cached credentials for the application
|
16
27
|
"""
|
17
28
|
|
18
|
-
def __init__(self, name: str, **kwargs):
|
29
|
+
def __init__(self, name: str, **kwargs: Any) -> None:
|
30
|
+
"""
|
31
|
+
Initialize the base application.
|
32
|
+
|
33
|
+
Args:
|
34
|
+
name: The name of the application
|
35
|
+
**kwargs: Additional keyword arguments passed to the application
|
36
|
+
"""
|
19
37
|
self.name = name
|
20
38
|
logger.debug(f"Initializing Application '{name}' with kwargs: {kwargs}")
|
21
39
|
analytics.track_app_loaded(name) # Track app loading
|
22
40
|
|
23
41
|
@abstractmethod
|
24
|
-
def list_tools(self):
|
42
|
+
def list_tools(self) -> list[Callable]:
|
43
|
+
"""
|
44
|
+
List all available tools for the application.
|
45
|
+
|
46
|
+
Returns:
|
47
|
+
List[Any]: A list of tools available in the application
|
48
|
+
"""
|
25
49
|
pass
|
26
50
|
|
27
51
|
|
28
52
|
class APIApplication(BaseApplication):
|
29
53
|
"""
|
30
|
-
|
54
|
+
Application that uses HTTP APIs to interact with external services.
|
55
|
+
|
56
|
+
This class provides a base implementation for applications that communicate
|
57
|
+
with external services via HTTP APIs. It handles authentication, request
|
58
|
+
management, and response processing.
|
59
|
+
|
60
|
+
Attributes:
|
61
|
+
name (str): The name of the application
|
62
|
+
integration (Optional[Integration]): The integration configuration
|
63
|
+
default_timeout (int): Default timeout for HTTP requests in seconds
|
64
|
+
base_url (str): Base URL for API requests
|
31
65
|
"""
|
32
66
|
|
33
|
-
def __init__(
|
67
|
+
def __init__(
|
68
|
+
self,
|
69
|
+
name: str,
|
70
|
+
integration: Integration | None = None,
|
71
|
+
client: httpx.Client | None = None,
|
72
|
+
**kwargs: Any,
|
73
|
+
) -> None:
|
74
|
+
"""
|
75
|
+
Initialize the API application.
|
76
|
+
|
77
|
+
Args:
|
78
|
+
name: The name of the application
|
79
|
+
integration: Optional integration configuration
|
80
|
+
**kwargs: Additional keyword arguments
|
81
|
+
"""
|
34
82
|
super().__init__(name, **kwargs)
|
35
|
-
self.default_timeout = 180
|
36
|
-
self.integration = integration
|
83
|
+
self.default_timeout: int = 180
|
84
|
+
self.integration: Integration | None = integration
|
37
85
|
logger.debug(
|
38
86
|
f"Initializing APIApplication '{name}' with integration: {integration}"
|
39
87
|
)
|
40
|
-
self._client =
|
41
|
-
# base_url should be set by subclasses, e.g., self.base_url = "https://api.example.com"
|
88
|
+
self._client: httpx.Client | None = client
|
42
89
|
self.base_url: str = "" # Initialize, but subclasses should set this
|
43
90
|
|
44
|
-
def _get_headers(self):
|
91
|
+
def _get_headers(self) -> dict[str, str]:
|
92
|
+
"""
|
93
|
+
Get the headers for API requests.
|
94
|
+
|
95
|
+
This method constructs the appropriate headers based on the available
|
96
|
+
credentials. It supports various authentication methods including
|
97
|
+
direct headers, API keys, and access tokens.
|
98
|
+
|
99
|
+
Returns:
|
100
|
+
Dict[str, str]: Headers to be used in API requests
|
101
|
+
"""
|
45
102
|
if not self.integration:
|
46
103
|
logger.debug("No integration configured, returning empty headers")
|
47
104
|
return {}
|
48
105
|
credentials = self.integration.get_credentials()
|
49
|
-
logger.debug(
|
106
|
+
logger.debug("Got credentials for integration")
|
50
107
|
|
51
108
|
# Check if direct headers are provided
|
52
109
|
headers = credentials.get("headers")
|
@@ -79,36 +136,74 @@ class APIApplication(BaseApplication):
|
|
79
136
|
return {}
|
80
137
|
|
81
138
|
@property
|
82
|
-
def client(self):
|
139
|
+
def client(self) -> httpx.Client:
|
140
|
+
"""
|
141
|
+
Get the HTTP client instance.
|
142
|
+
|
143
|
+
This property ensures that the HTTP client is properly initialized
|
144
|
+
with the correct base URL and headers.
|
145
|
+
|
146
|
+
Returns:
|
147
|
+
httpx.Client: The initialized HTTP client
|
148
|
+
"""
|
83
149
|
if not self._client:
|
84
150
|
headers = self._get_headers()
|
85
151
|
if not self.base_url:
|
86
152
|
logger.warning(f"APIApplication '{self.name}' base_url is not set.")
|
87
|
-
# Fallback: Initialize client without base_url, requiring full URLs in methods
|
88
153
|
self._client = httpx.Client(
|
89
154
|
headers=headers, timeout=self.default_timeout
|
90
155
|
)
|
91
156
|
else:
|
92
157
|
self._client = httpx.Client(
|
93
|
-
base_url=self.base_url,
|
158
|
+
base_url=self.base_url,
|
94
159
|
headers=headers,
|
95
160
|
timeout=self.default_timeout,
|
96
161
|
)
|
97
162
|
return self._client
|
98
163
|
|
99
|
-
def _get(self, url, params=None):
|
164
|
+
def _get(self, url: str, params: dict[str, Any] | None = None) -> httpx.Response:
|
165
|
+
"""
|
166
|
+
Make a GET request to the specified URL.
|
167
|
+
|
168
|
+
Args:
|
169
|
+
url: The URL to send the request to
|
170
|
+
params: Optional query parameters
|
171
|
+
|
172
|
+
Returns:
|
173
|
+
httpx.Response: The response from the server
|
174
|
+
|
175
|
+
Raises:
|
176
|
+
httpx.HTTPError: If the request fails
|
177
|
+
"""
|
100
178
|
logger.debug(f"Making GET request to {url} with params: {params}")
|
101
179
|
response = self.client.get(url, params=params)
|
102
180
|
response.raise_for_status()
|
103
181
|
logger.debug(f"GET request successful with status code: {response.status_code}")
|
104
182
|
return response
|
105
183
|
|
106
|
-
def _post(
|
184
|
+
def _post(
|
185
|
+
self, url: str, data: dict[str, Any], params: dict[str, Any] | None = None
|
186
|
+
) -> httpx.Response:
|
187
|
+
"""
|
188
|
+
Make a POST request to the specified URL.
|
189
|
+
|
190
|
+
Args:
|
191
|
+
url: The URL to send the request to
|
192
|
+
data: The data to send in the request body
|
193
|
+
params: Optional query parameters
|
194
|
+
|
195
|
+
Returns:
|
196
|
+
httpx.Response: The response from the server
|
197
|
+
|
198
|
+
Raises:
|
199
|
+
httpx.HTTPError: If the request fails
|
200
|
+
"""
|
107
201
|
logger.debug(
|
108
202
|
f"Making POST request to {url} with params: {params} and data: {data}"
|
109
203
|
)
|
110
|
-
response =
|
204
|
+
response = httpx.post(
|
111
205
|
url,
|
206
|
+
headers=self._get_headers(),
|
112
207
|
json=data,
|
113
208
|
params=params,
|
114
209
|
)
|
@@ -118,7 +213,23 @@ class APIApplication(BaseApplication):
|
|
118
213
|
)
|
119
214
|
return response
|
120
215
|
|
121
|
-
def _put(
|
216
|
+
def _put(
|
217
|
+
self, url: str, data: dict[str, Any], params: dict[str, Any] | None = None
|
218
|
+
) -> httpx.Response:
|
219
|
+
"""
|
220
|
+
Make a PUT request to the specified URL.
|
221
|
+
|
222
|
+
Args:
|
223
|
+
url: The URL to send the request to
|
224
|
+
data: The data to send in the request body
|
225
|
+
params: Optional query parameters
|
226
|
+
|
227
|
+
Returns:
|
228
|
+
httpx.Response: The response from the server
|
229
|
+
|
230
|
+
Raises:
|
231
|
+
httpx.HTTPError: If the request fails
|
232
|
+
"""
|
122
233
|
logger.debug(
|
123
234
|
f"Making PUT request to {url} with params: {params} and data: {data}"
|
124
235
|
)
|
@@ -131,8 +242,20 @@ class APIApplication(BaseApplication):
|
|
131
242
|
logger.debug(f"PUT request successful with status code: {response.status_code}")
|
132
243
|
return response
|
133
244
|
|
134
|
-
def _delete(self, url, params=None):
|
135
|
-
|
245
|
+
def _delete(self, url: str, params: dict[str, Any] | None = None) -> httpx.Response:
|
246
|
+
"""
|
247
|
+
Make a DELETE request to the specified URL.
|
248
|
+
|
249
|
+
Args:
|
250
|
+
url: The URL to send the request to
|
251
|
+
params: Optional query parameters
|
252
|
+
|
253
|
+
Returns:
|
254
|
+
httpx.Response: The response from the server
|
255
|
+
|
256
|
+
Raises:
|
257
|
+
httpx.HTTPError: If the request fails
|
258
|
+
"""
|
136
259
|
logger.debug(f"Making DELETE request to {url} with params: {params}")
|
137
260
|
response = self.client.delete(url, params=params, timeout=self.default_timeout)
|
138
261
|
response.raise_for_status()
|
@@ -141,8 +264,23 @@ class APIApplication(BaseApplication):
|
|
141
264
|
)
|
142
265
|
return response
|
143
266
|
|
144
|
-
def _patch(
|
145
|
-
|
267
|
+
def _patch(
|
268
|
+
self, url: str, data: dict[str, Any], params: dict[str, Any] | None = None
|
269
|
+
) -> httpx.Response:
|
270
|
+
"""
|
271
|
+
Make a PATCH request to the specified URL.
|
272
|
+
|
273
|
+
Args:
|
274
|
+
url: The URL to send the request to
|
275
|
+
data: The data to send in the request body
|
276
|
+
params: Optional query parameters
|
277
|
+
|
278
|
+
Returns:
|
279
|
+
httpx.Response: The response from the server
|
280
|
+
|
281
|
+
Raises:
|
282
|
+
httpx.HTTPError: If the request fails
|
283
|
+
"""
|
146
284
|
logger.debug(
|
147
285
|
f"Making PATCH request to {url} with params: {params} and data: {data}"
|
148
286
|
)
|
@@ -157,25 +295,55 @@ class APIApplication(BaseApplication):
|
|
157
295
|
)
|
158
296
|
return response
|
159
297
|
|
160
|
-
def validate(self):
|
161
|
-
pass
|
162
|
-
|
163
298
|
|
164
299
|
class GraphQLApplication(BaseApplication):
|
165
300
|
"""
|
166
|
-
|
301
|
+
Application that uses GraphQL to interact with external services.
|
302
|
+
|
303
|
+
This class provides a base implementation for applications that communicate
|
304
|
+
with external services via GraphQL. It handles authentication, query execution,
|
305
|
+
and response processing.
|
306
|
+
|
307
|
+
Attributes:
|
308
|
+
name (str): The name of the application
|
309
|
+
base_url (str): Base URL for GraphQL endpoint
|
310
|
+
integration (Optional[Integration]): The integration configuration
|
167
311
|
"""
|
168
312
|
|
169
313
|
def __init__(
|
170
|
-
self,
|
171
|
-
|
314
|
+
self,
|
315
|
+
name: str,
|
316
|
+
base_url: str,
|
317
|
+
integration: Integration | None = None,
|
318
|
+
client: GraphQLClient | None = None,
|
319
|
+
**kwargs: Any,
|
320
|
+
) -> None:
|
321
|
+
"""
|
322
|
+
Initialize the GraphQL application.
|
323
|
+
|
324
|
+
Args:
|
325
|
+
name: The name of the application
|
326
|
+
base_url: The base URL for the GraphQL endpoint
|
327
|
+
integration: Optional integration configuration
|
328
|
+
**kwargs: Additional keyword arguments
|
329
|
+
"""
|
172
330
|
super().__init__(name, **kwargs)
|
173
331
|
self.base_url = base_url
|
332
|
+
self.integration = integration
|
174
333
|
logger.debug(f"Initializing Application '{name}' with kwargs: {kwargs}")
|
175
|
-
|
176
|
-
|
334
|
+
self._client: GraphQLClient | None = client
|
335
|
+
|
336
|
+
def _get_headers(self) -> dict[str, str]:
|
337
|
+
"""
|
338
|
+
Get the headers for GraphQL requests.
|
177
339
|
|
178
|
-
|
340
|
+
This method constructs the appropriate headers based on the available
|
341
|
+
credentials. It supports various authentication methods including
|
342
|
+
direct headers, API keys, and access tokens.
|
343
|
+
|
344
|
+
Returns:
|
345
|
+
Dict[str, str]: Headers to be used in GraphQL requests
|
346
|
+
"""
|
179
347
|
if not self.integration:
|
180
348
|
logger.debug("No integration configured, returning empty headers")
|
181
349
|
return {}
|
@@ -211,23 +379,64 @@ class GraphQLApplication(BaseApplication):
|
|
211
379
|
return {}
|
212
380
|
|
213
381
|
@property
|
214
|
-
def client(self):
|
382
|
+
def client(self) -> GraphQLClient:
|
383
|
+
"""
|
384
|
+
Get the GraphQL client instance.
|
385
|
+
|
386
|
+
This property ensures that the GraphQL client is properly initialized
|
387
|
+
with the correct transport and headers.
|
388
|
+
|
389
|
+
Returns:
|
390
|
+
Client: The initialized GraphQL client
|
391
|
+
"""
|
215
392
|
if not self._client:
|
216
393
|
headers = self._get_headers()
|
217
394
|
transport = RequestsHTTPTransport(url=self.base_url, headers=headers)
|
218
|
-
self._client =
|
395
|
+
self._client = GraphQLClient(
|
396
|
+
transport=transport, fetch_schema_from_transport=True
|
397
|
+
)
|
219
398
|
return self._client
|
220
399
|
|
221
|
-
def mutate(
|
400
|
+
def mutate(
|
401
|
+
self,
|
402
|
+
mutation: str | DocumentNode,
|
403
|
+
variables: dict[str, Any] | None = None,
|
404
|
+
) -> dict[str, Any]:
|
405
|
+
"""
|
406
|
+
Execute a GraphQL mutation.
|
407
|
+
|
408
|
+
Args:
|
409
|
+
mutation: The GraphQL mutation string or DocumentNode
|
410
|
+
variables: Optional variables for the mutation
|
411
|
+
|
412
|
+
Returns:
|
413
|
+
Dict[str, Any]: The result of the mutation
|
414
|
+
|
415
|
+
Raises:
|
416
|
+
Exception: If the mutation execution fails
|
417
|
+
"""
|
222
418
|
if isinstance(mutation, str):
|
223
419
|
mutation = gql(mutation)
|
224
420
|
return self.client.execute(mutation, variable_values=variables)
|
225
421
|
|
226
|
-
def query(
|
422
|
+
def query(
|
423
|
+
self,
|
424
|
+
query: str | DocumentNode,
|
425
|
+
variables: dict[str, Any] | None = None,
|
426
|
+
) -> dict[str, Any]:
|
427
|
+
"""
|
428
|
+
Execute a GraphQL query.
|
429
|
+
|
430
|
+
Args:
|
431
|
+
query: The GraphQL query string or DocumentNode
|
432
|
+
variables: Optional variables for the query
|
433
|
+
|
434
|
+
Returns:
|
435
|
+
Dict[str, Any]: The result of the query
|
436
|
+
|
437
|
+
Raises:
|
438
|
+
Exception: If the query execution fails
|
439
|
+
"""
|
227
440
|
if isinstance(query, str):
|
228
441
|
query = gql(query)
|
229
442
|
return self.client.execute(query, variable_values=variables)
|
230
|
-
|
231
|
-
@abstractmethod
|
232
|
-
def list_tools(self):
|
233
|
-
pass
|