universal-mcp 0.1.8rc2__py3-none-any.whl → 0.1.8rc4__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/__init__.py +0 -2
- universal_mcp/analytics.py +75 -0
- universal_mcp/applications/ahrefs/README.md +76 -0
- universal_mcp/applications/ahrefs/__init__.py +0 -0
- universal_mcp/applications/ahrefs/app.py +2291 -0
- universal_mcp/applications/application.py +94 -5
- universal_mcp/applications/calendly/app.py +412 -171
- universal_mcp/applications/coda/README.md +133 -0
- universal_mcp/applications/coda/__init__.py +0 -0
- universal_mcp/applications/coda/app.py +3671 -0
- universal_mcp/applications/e2b/app.py +8 -35
- universal_mcp/applications/figma/README.md +74 -0
- universal_mcp/applications/figma/__init__.py +0 -0
- universal_mcp/applications/figma/app.py +1261 -0
- universal_mcp/applications/firecrawl/app.py +3 -33
- universal_mcp/applications/github/app.py +41 -42
- universal_mcp/applications/google_calendar/app.py +20 -31
- universal_mcp/applications/google_docs/app.py +21 -46
- universal_mcp/applications/google_drive/app.py +53 -76
- universal_mcp/applications/google_mail/app.py +40 -56
- universal_mcp/applications/google_sheet/app.py +43 -68
- universal_mcp/applications/markitdown/app.py +4 -4
- universal_mcp/applications/notion/app.py +93 -83
- universal_mcp/applications/perplexity/app.py +4 -38
- universal_mcp/applications/reddit/app.py +32 -32
- universal_mcp/applications/resend/app.py +4 -22
- universal_mcp/applications/serpapi/app.py +6 -32
- universal_mcp/applications/tavily/app.py +4 -24
- universal_mcp/applications/wrike/app.py +565 -237
- universal_mcp/applications/youtube/app.py +625 -183
- universal_mcp/applications/zenquotes/app.py +3 -3
- universal_mcp/exceptions.py +1 -0
- universal_mcp/integrations/__init__.py +11 -2
- universal_mcp/integrations/agentr.py +27 -4
- universal_mcp/integrations/integration.py +14 -6
- universal_mcp/logger.py +3 -56
- universal_mcp/servers/__init__.py +2 -1
- universal_mcp/servers/server.py +73 -77
- universal_mcp/stores/store.py +5 -3
- universal_mcp/tools/__init__.py +1 -1
- universal_mcp/tools/adapters.py +4 -1
- universal_mcp/tools/func_metadata.py +5 -6
- universal_mcp/tools/tools.py +108 -51
- universal_mcp/utils/docgen.py +121 -69
- universal_mcp/utils/docstring_parser.py +44 -21
- universal_mcp/utils/dump_app_tools.py +33 -23
- universal_mcp/utils/installation.py +199 -8
- universal_mcp/utils/openapi.py +121 -47
- {universal_mcp-0.1.8rc2.dist-info → universal_mcp-0.1.8rc4.dist-info}/METADATA +2 -2
- universal_mcp-0.1.8rc4.dist-info/RECORD +81 -0
- universal_mcp-0.1.8rc2.dist-info/RECORD +0 -71
- {universal_mcp-0.1.8rc2.dist-info → universal_mcp-0.1.8rc4.dist-info}/WHEEL +0 -0
- {universal_mcp-0.1.8rc2.dist-info → universal_mcp-0.1.8rc4.dist-info}/entry_points.txt +0 -0
@@ -8,16 +8,16 @@ class ZenquotesApp(APIApplication):
|
|
8
8
|
def get_quote(self) -> str:
|
9
9
|
"""
|
10
10
|
Fetches a random inspirational quote from the Zen Quotes API.
|
11
|
-
|
11
|
+
|
12
12
|
Returns:
|
13
13
|
A formatted string containing the quote and its author in the format 'quote - author'
|
14
|
-
|
14
|
+
|
15
15
|
Raises:
|
16
16
|
RequestException: If the HTTP request to the Zen Quotes API fails
|
17
17
|
JSONDecodeError: If the API response contains invalid JSON
|
18
18
|
IndexError: If the API response doesn't contain any quotes
|
19
19
|
KeyError: If the quote data doesn't contain the expected 'q' or 'a' fields
|
20
|
-
|
20
|
+
|
21
21
|
Tags:
|
22
22
|
fetch, quotes, api, http, important
|
23
23
|
"""
|
universal_mcp/exceptions.py
CHANGED
@@ -8,7 +8,9 @@ from universal_mcp.integrations.integration import (
|
|
8
8
|
from universal_mcp.stores.store import BaseStore
|
9
9
|
|
10
10
|
|
11
|
-
def integration_from_config(
|
11
|
+
def integration_from_config(
|
12
|
+
config: IntegrationConfig, store: BaseStore | None = None, **kwargs
|
13
|
+
) -> Integration:
|
12
14
|
if config.type == "api_key":
|
13
15
|
return ApiKeyIntegration(config.name, store=store, **kwargs)
|
14
16
|
elif config.type == "agentr":
|
@@ -19,4 +21,11 @@ def integration_from_config(config: IntegrationConfig, store: BaseStore | None =
|
|
19
21
|
else:
|
20
22
|
raise ValueError(f"Unsupported integration type: {config.type}")
|
21
23
|
|
22
|
-
|
24
|
+
|
25
|
+
__all__ = [
|
26
|
+
"AgentRIntegration",
|
27
|
+
"Integration",
|
28
|
+
"ApiKeyIntegration",
|
29
|
+
"OAuthIntegration",
|
30
|
+
"integration_from_config",
|
31
|
+
]
|
@@ -29,7 +29,10 @@ class AgentRIntegration(Integration):
|
|
29
29
|
"API key for AgentR is missing. Please visit https://agentr.dev to create an API key, then set it as AGENTR_API_KEY environment variable."
|
30
30
|
)
|
31
31
|
raise ValueError("AgentR API key required - get one at https://agentr.dev")
|
32
|
-
self.base_url = os.getenv("AGENTR_BASE_URL", "https://api.agentr.dev")
|
32
|
+
self.base_url = os.getenv("AGENTR_BASE_URL", "https://api.agentr.dev").rstrip(
|
33
|
+
"/"
|
34
|
+
)
|
35
|
+
self._credentials = None
|
33
36
|
|
34
37
|
def set_credentials(self, credentials: dict | None = None):
|
35
38
|
"""Set credentials for the integration.
|
@@ -43,9 +46,9 @@ class AgentRIntegration(Integration):
|
|
43
46
|
str: Authorization URL from authorize() method
|
44
47
|
"""
|
45
48
|
return self.authorize()
|
46
|
-
# raise NotImplementedError("AgentR Integration does not support setting credentials. Visit the authorize url to set credentials.")
|
47
49
|
|
48
|
-
|
50
|
+
@property
|
51
|
+
def credentials(self):
|
49
52
|
"""Get credentials for the integration from the AgentR API.
|
50
53
|
|
51
54
|
Makes API request to retrieve stored credentials for this integration.
|
@@ -57,16 +60,36 @@ class AgentRIntegration(Integration):
|
|
57
60
|
NotAuthorizedError: If credentials are not found (404 response)
|
58
61
|
HTTPError: For other API errors
|
59
62
|
"""
|
63
|
+
if self._credentials is not None:
|
64
|
+
return self._credentials
|
60
65
|
response = httpx.get(
|
61
66
|
f"{self.base_url}/api/{self.name}/credentials/",
|
62
67
|
headers={"accept": "application/json", "X-API-KEY": self.api_key},
|
63
68
|
)
|
64
69
|
if response.status_code == 404:
|
70
|
+
logger.warning(
|
71
|
+
f"No credentials found for {self.name}. Requesting authorization..."
|
72
|
+
)
|
65
73
|
action = self.authorize()
|
66
74
|
raise NotAuthorizedError(action)
|
67
75
|
response.raise_for_status()
|
68
76
|
data = response.json()
|
69
|
-
|
77
|
+
self._credentials = data
|
78
|
+
return self._credentials
|
79
|
+
|
80
|
+
def get_credentials(self):
|
81
|
+
"""Get credentials for the integration from the AgentR API.
|
82
|
+
|
83
|
+
Makes API request to retrieve stored credentials for this integration.
|
84
|
+
|
85
|
+
Returns:
|
86
|
+
dict: Credentials data from API response
|
87
|
+
|
88
|
+
Raises:
|
89
|
+
NotAuthorizedError: If credentials are not found (404 response)
|
90
|
+
HTTPError: For other API errors
|
91
|
+
"""
|
92
|
+
return self.credentials
|
70
93
|
|
71
94
|
def authorize(self):
|
72
95
|
"""Get authorization URL for the integration.
|
@@ -91,9 +91,22 @@ class ApiKeyIntegration(Integration):
|
|
91
91
|
"""
|
92
92
|
|
93
93
|
def __init__(self, name: str, store: BaseStore | None = None, **kwargs):
|
94
|
+
self.type = "api_key"
|
94
95
|
sanitized_name = sanitize_api_key_name(name)
|
95
96
|
super().__init__(sanitized_name, store, **kwargs)
|
96
97
|
logger.info(f"Initializing API Key Integration: {name} with store: {store}")
|
98
|
+
self._api_key: str | None = None
|
99
|
+
|
100
|
+
@property
|
101
|
+
def api_key(self) -> str | None:
|
102
|
+
if not self._api_key:
|
103
|
+
try:
|
104
|
+
credentials = self.store.get(self.name)
|
105
|
+
self.api_key = credentials
|
106
|
+
except KeyNotFoundError as e:
|
107
|
+
action = self.authorize()
|
108
|
+
raise NotAuthorizedError(action) from e
|
109
|
+
return self._api_key
|
97
110
|
|
98
111
|
def get_credentials(self) -> dict[str, str]:
|
99
112
|
"""Get API key credentials.
|
@@ -104,12 +117,7 @@ class ApiKeyIntegration(Integration):
|
|
104
117
|
Raises:
|
105
118
|
NotAuthorizedError: If API key is not found.
|
106
119
|
"""
|
107
|
-
|
108
|
-
credentials = self.store.get(self.name)
|
109
|
-
except KeyNotFoundError:
|
110
|
-
action = self.authorize()
|
111
|
-
raise NotAuthorizedError(action)
|
112
|
-
return {"api_key": credentials}
|
120
|
+
return {"api_key": self.api_key}
|
113
121
|
|
114
122
|
def set_credentials(self, credentials: dict[str, Any]) -> None:
|
115
123
|
"""Set API key credentials.
|
universal_mcp/logger.py
CHANGED
@@ -1,70 +1,17 @@
|
|
1
|
-
import os
|
2
1
|
import sys
|
3
|
-
import uuid
|
4
|
-
from functools import lru_cache
|
5
2
|
|
6
3
|
from loguru import logger
|
7
4
|
|
8
5
|
|
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
|
-
return version("universal_mcp")
|
18
|
-
except ImportError:
|
19
|
-
return "unknown"
|
20
|
-
|
21
|
-
|
22
|
-
def get_user_id():
|
23
|
-
"""
|
24
|
-
Generate a unique user ID for the current session
|
25
|
-
"""
|
26
|
-
return "universal_" + str(uuid.uuid4())[:8]
|
27
|
-
|
28
|
-
|
29
|
-
def posthog_sink(message, user_id=get_user_id()):
|
30
|
-
"""
|
31
|
-
Custom sink for sending logs to PostHog
|
32
|
-
"""
|
33
|
-
try:
|
34
|
-
import posthog
|
35
|
-
|
36
|
-
posthog.host = "https://us.i.posthog.com"
|
37
|
-
posthog.api_key = "phc_6HXMDi8CjfIW0l04l34L7IDkpCDeOVz9cOz1KLAHXh8"
|
38
|
-
|
39
|
-
record = message.record
|
40
|
-
properties = {
|
41
|
-
"level": record["level"].name,
|
42
|
-
"module": record["name"],
|
43
|
-
"function": record["function"],
|
44
|
-
"line": record["line"],
|
45
|
-
"message": record["message"],
|
46
|
-
"version": get_version(),
|
47
|
-
}
|
48
|
-
posthog.capture(user_id, "universal_mcp", properties)
|
49
|
-
except Exception:
|
50
|
-
# Silently fail if PostHog capture fails - don't want logging to break the app
|
51
|
-
pass
|
52
|
-
|
53
|
-
|
54
6
|
def setup_logger():
|
55
7
|
logger.remove()
|
8
|
+
# STDOUT cant be used as a sink because it will break the stream
|
56
9
|
# logger.add(
|
57
10
|
# sink=sys.stdout,
|
58
|
-
# 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>",
|
59
11
|
# level="INFO",
|
60
|
-
# colorize=True,
|
61
12
|
# )
|
13
|
+
# STDERR
|
62
14
|
logger.add(
|
63
15
|
sink=sys.stderr,
|
64
|
-
|
65
|
-
level="WARNING",
|
66
|
-
colorize=True,
|
16
|
+
level="INFO",
|
67
17
|
)
|
68
|
-
telemetry_enabled = os.getenv("TELEMETRY_ENABLED", "true").lower() == "true"
|
69
|
-
if telemetry_enabled:
|
70
|
-
logger.add(posthog_sink, level="INFO") # PostHog telemetry
|
universal_mcp/servers/server.py
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
import os
|
2
2
|
from abc import ABC, abstractmethod
|
3
|
-
from
|
3
|
+
from collections.abc import Callable
|
4
|
+
from typing import Any
|
4
5
|
from urllib.parse import urlparse
|
5
6
|
|
6
7
|
import httpx
|
@@ -10,18 +11,17 @@ from mcp.types import TextContent
|
|
10
11
|
|
11
12
|
from universal_mcp.applications import Application, app_from_slug
|
12
13
|
from universal_mcp.config import AppConfig, ServerConfig, StoreConfig
|
13
|
-
from universal_mcp.
|
14
|
-
from universal_mcp.integrations import integration_from_config
|
14
|
+
from universal_mcp.integrations import AgentRIntegration, integration_from_config
|
15
15
|
from universal_mcp.stores import BaseStore, store_from_config
|
16
16
|
from universal_mcp.tools.tools import ToolManager
|
17
17
|
|
18
18
|
|
19
19
|
class BaseServer(FastMCP, ABC):
|
20
20
|
"""Base server class with common functionality.
|
21
|
-
|
21
|
+
|
22
22
|
This class provides core server functionality including store setup,
|
23
23
|
tool management, and application loading.
|
24
|
-
|
24
|
+
|
25
25
|
Args:
|
26
26
|
config: Server configuration
|
27
27
|
**kwargs: Additional keyword arguments passed to FastMCP
|
@@ -29,29 +29,12 @@ class BaseServer(FastMCP, ABC):
|
|
29
29
|
|
30
30
|
def __init__(self, config: ServerConfig, **kwargs):
|
31
31
|
super().__init__(config.name, config.description, **kwargs)
|
32
|
-
logger.info(
|
33
|
-
|
32
|
+
logger.info(
|
33
|
+
f"Initializing server: {config.name} ({config.type}) with store: {config.store}"
|
34
|
+
)
|
35
|
+
|
34
36
|
self.config = config # Store config at base level for consistency
|
35
37
|
self._tool_manager = ToolManager(warn_on_duplicate_tools=True)
|
36
|
-
self.store = self._setup_store(config.store)
|
37
|
-
self._load_apps()
|
38
|
-
|
39
|
-
def _setup_store(self, store_config: StoreConfig | None) -> BaseStore | None:
|
40
|
-
"""Setup and configure the store.
|
41
|
-
|
42
|
-
Args:
|
43
|
-
store_config: Store configuration
|
44
|
-
|
45
|
-
Returns:
|
46
|
-
Configured store instance or None if no config provided
|
47
|
-
"""
|
48
|
-
if not store_config:
|
49
|
-
return None
|
50
|
-
|
51
|
-
store = store_from_config(store_config)
|
52
|
-
self.add_tool(store.set)
|
53
|
-
self.add_tool(store.delete)
|
54
|
-
return store
|
55
38
|
|
56
39
|
@abstractmethod
|
57
40
|
def _load_apps(self) -> None:
|
@@ -60,7 +43,7 @@ class BaseServer(FastMCP, ABC):
|
|
60
43
|
|
61
44
|
def add_tool(self, tool: Callable) -> None:
|
62
45
|
"""Add a tool to the server.
|
63
|
-
|
46
|
+
|
64
47
|
Args:
|
65
48
|
tool: Tool to add
|
66
49
|
"""
|
@@ -68,68 +51,57 @@ class BaseServer(FastMCP, ABC):
|
|
68
51
|
|
69
52
|
async def list_tools(self) -> list[dict]:
|
70
53
|
"""List all available tools in MCP format.
|
71
|
-
|
54
|
+
|
72
55
|
Returns:
|
73
56
|
List of tool definitions
|
74
57
|
"""
|
75
|
-
return self._tool_manager.list_tools(format=
|
76
|
-
|
58
|
+
return self._tool_manager.list_tools(format="mcp")
|
59
|
+
|
77
60
|
def _format_tool_result(self, result: Any) -> list[TextContent]:
|
78
61
|
"""Format tool result into TextContent list.
|
79
|
-
|
62
|
+
|
80
63
|
Args:
|
81
64
|
result: Raw tool result
|
82
|
-
|
65
|
+
|
83
66
|
Returns:
|
84
67
|
List of TextContent objects
|
85
68
|
"""
|
86
69
|
if isinstance(result, str):
|
87
70
|
return [TextContent(type="text", text=result)]
|
88
|
-
elif isinstance(result, list) and all(
|
71
|
+
elif isinstance(result, list) and all(
|
72
|
+
isinstance(item, TextContent) for item in result
|
73
|
+
):
|
89
74
|
return result
|
90
75
|
else:
|
91
|
-
logger.warning(
|
76
|
+
logger.warning(
|
77
|
+
f"Tool returned unexpected type: {type(result)}. Wrapping in TextContent."
|
78
|
+
)
|
92
79
|
return [TextContent(type="text", text=str(result))]
|
93
|
-
|
94
|
-
async def call_tool(
|
80
|
+
|
81
|
+
async def call_tool(
|
82
|
+
self, name: str, arguments: dict[str, Any]
|
83
|
+
) -> list[TextContent]:
|
95
84
|
"""Call a tool with comprehensive error handling.
|
96
|
-
|
85
|
+
|
97
86
|
Args:
|
98
87
|
name: Tool name
|
99
88
|
arguments: Tool arguments
|
100
|
-
|
89
|
+
|
101
90
|
Returns:
|
102
91
|
List of TextContent results
|
103
|
-
|
92
|
+
|
104
93
|
Raises:
|
105
94
|
ToolError: If tool execution fails
|
106
95
|
"""
|
107
96
|
logger.info(f"Calling tool: {name} with arguments: {arguments}")
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
return self._format_tool_result(result)
|
112
|
-
|
113
|
-
except ToolError as e:
|
114
|
-
if isinstance(e.__cause__, NotAuthorizedError):
|
115
|
-
message = f"Not authorized to call tool {name}: {e.__cause__.message}"
|
116
|
-
logger.warning(message)
|
117
|
-
return [TextContent(type="text", text=message)]
|
118
|
-
elif isinstance(e.__cause__, httpx.HTTPError):
|
119
|
-
message = f"HTTP error calling tool {name}: {str(e.__cause__)}"
|
120
|
-
logger.error(message)
|
121
|
-
return [TextContent(type="text", text=message)]
|
122
|
-
elif isinstance(e.__cause__, ValueError):
|
123
|
-
message = f"Invalid arguments for tool {name}: {str(e.__cause__)}"
|
124
|
-
logger.error(message)
|
125
|
-
return [TextContent(type="text", text=message)]
|
126
|
-
logger.error(f"Error calling tool {name}: {str(e)}", exc_info=True)
|
127
|
-
raise
|
97
|
+
result = await self._tool_manager.call_tool(name, arguments)
|
98
|
+
logger.info(f"Tool '{name}' completed successfully")
|
99
|
+
return self._format_tool_result(result)
|
128
100
|
|
129
101
|
|
130
102
|
class LocalServer(BaseServer):
|
131
103
|
"""Local development server implementation.
|
132
|
-
|
104
|
+
|
133
105
|
Args:
|
134
106
|
config: Server configuration
|
135
107
|
**kwargs: Additional keyword arguments passed to FastMCP
|
@@ -137,21 +109,41 @@ class LocalServer(BaseServer):
|
|
137
109
|
|
138
110
|
def __init__(self, config: ServerConfig, **kwargs):
|
139
111
|
super().__init__(config, **kwargs)
|
112
|
+
self.store = self._setup_store(config.store)
|
113
|
+
self._load_apps()
|
114
|
+
|
115
|
+
def _setup_store(self, store_config: StoreConfig | None) -> BaseStore | None:
|
116
|
+
"""Setup and configure the store.
|
117
|
+
|
118
|
+
Args:
|
119
|
+
store_config: Store configuration
|
120
|
+
|
121
|
+
Returns:
|
122
|
+
Configured store instance or None if no config provided
|
123
|
+
"""
|
124
|
+
if not store_config:
|
125
|
+
return None
|
126
|
+
|
127
|
+
store = store_from_config(store_config)
|
128
|
+
self.add_tool(store.set)
|
129
|
+
self.add_tool(store.delete)
|
130
|
+
return store
|
140
131
|
|
141
132
|
def _load_app(self, app_config: AppConfig) -> Application | None:
|
142
133
|
"""Load a single application with its integration.
|
143
|
-
|
134
|
+
|
144
135
|
Args:
|
145
136
|
app_config: Application configuration
|
146
|
-
|
137
|
+
|
147
138
|
Returns:
|
148
139
|
Configured application instance or None if loading fails
|
149
140
|
"""
|
150
141
|
try:
|
151
|
-
integration =
|
152
|
-
app_config.integration,
|
153
|
-
|
154
|
-
|
142
|
+
integration = (
|
143
|
+
integration_from_config(app_config.integration, store=self.store)
|
144
|
+
if app_config.integration
|
145
|
+
else None
|
146
|
+
)
|
155
147
|
return app_from_slug(app_config.name)(integration=integration)
|
156
148
|
except Exception as e:
|
157
149
|
logger.error(f"Failed to load app {app_config.name}: {e}", exc_info=True)
|
@@ -168,7 +160,7 @@ class LocalServer(BaseServer):
|
|
168
160
|
|
169
161
|
class AgentRServer(BaseServer):
|
170
162
|
"""AgentR API-connected server implementation.
|
171
|
-
|
163
|
+
|
172
164
|
Args:
|
173
165
|
config: Server configuration
|
174
166
|
api_key: Optional API key for AgentR authentication. If not provided,
|
@@ -179,21 +171,22 @@ class AgentRServer(BaseServer):
|
|
179
171
|
def __init__(self, config: ServerConfig, api_key: str | None = None, **kwargs):
|
180
172
|
self.api_key = api_key or os.getenv("AGENTR_API_KEY")
|
181
173
|
self.base_url = os.getenv("AGENTR_BASE_URL", "https://api.agentr.dev")
|
182
|
-
|
174
|
+
|
183
175
|
if not self.api_key:
|
184
176
|
raise ValueError("API key required - get one at https://agentr.dev")
|
185
177
|
parsed = urlparse(self.base_url)
|
186
178
|
if not all([parsed.scheme, parsed.netloc]):
|
187
179
|
raise ValueError(f"Invalid base URL format: {self.base_url}")
|
188
|
-
|
189
180
|
super().__init__(config, **kwargs)
|
190
|
-
|
181
|
+
self.integration = AgentRIntegration(name="agentr", api_key=self.api_key)
|
182
|
+
self._load_apps()
|
183
|
+
|
191
184
|
def _fetch_apps(self) -> list[AppConfig]:
|
192
185
|
"""Fetch available apps from AgentR API.
|
193
|
-
|
186
|
+
|
194
187
|
Returns:
|
195
188
|
List of application configurations
|
196
|
-
|
189
|
+
|
197
190
|
Raises:
|
198
191
|
httpx.HTTPError: If API request fails
|
199
192
|
"""
|
@@ -211,17 +204,20 @@ class AgentRServer(BaseServer):
|
|
211
204
|
|
212
205
|
def _load_app(self, app_config: AppConfig) -> Application | None:
|
213
206
|
"""Load a single application with AgentR integration.
|
214
|
-
|
207
|
+
|
215
208
|
Args:
|
216
209
|
app_config: Application configuration
|
217
|
-
|
210
|
+
|
218
211
|
Returns:
|
219
212
|
Configured application instance or None if loading fails
|
220
213
|
"""
|
221
214
|
try:
|
222
|
-
integration =
|
223
|
-
|
224
|
-
|
215
|
+
integration = (
|
216
|
+
AgentRIntegration(
|
217
|
+
name=app_config.integration.name, api_key=self.api_key
|
218
|
+
)
|
219
|
+
if app_config.integration
|
220
|
+
else None
|
225
221
|
)
|
226
222
|
return app_from_slug(app_config.name)(integration=integration)
|
227
223
|
except Exception as e:
|
universal_mcp/stores/store.py
CHANGED
@@ -8,11 +8,13 @@ from loguru import logger
|
|
8
8
|
|
9
9
|
class StoreError(Exception):
|
10
10
|
"""Base exception class for store-related errors."""
|
11
|
+
|
11
12
|
pass
|
12
13
|
|
13
14
|
|
14
15
|
class KeyNotFoundError(StoreError):
|
15
16
|
"""Exception raised when a key is not found in the store."""
|
17
|
+
|
16
18
|
pass
|
17
19
|
|
18
20
|
|
@@ -29,10 +31,10 @@ class BaseStore(ABC):
|
|
29
31
|
|
30
32
|
Args:
|
31
33
|
key (str): The key to look up
|
32
|
-
|
34
|
+
|
33
35
|
Returns:
|
34
36
|
Any: The stored value
|
35
|
-
|
37
|
+
|
36
38
|
Raises:
|
37
39
|
KeyNotFoundError: If the key is not found in the store
|
38
40
|
StoreError: If there is an error accessing the store
|
@@ -223,7 +225,7 @@ class KeyringStore(BaseStore):
|
|
223
225
|
keyring.set_password(self.app_name, key, value)
|
224
226
|
except Exception as e:
|
225
227
|
raise StoreError(f"Error storing in keyring: {str(e)}") from e
|
226
|
-
|
228
|
+
|
227
229
|
def delete(self, key: str) -> None:
|
228
230
|
"""
|
229
231
|
Delete a password from the system keyring.
|
universal_mcp/tools/__init__.py
CHANGED
universal_mcp/tools/adapters.py
CHANGED
@@ -5,16 +5,19 @@ def convert_tool_to_mcp_tool(
|
|
5
5
|
tool: Tool,
|
6
6
|
):
|
7
7
|
from mcp.server.fastmcp.server import MCPTool
|
8
|
+
|
8
9
|
return MCPTool(
|
9
10
|
name=tool.name,
|
10
11
|
description=tool.description or "",
|
11
12
|
inputSchema=tool.parameters,
|
12
13
|
)
|
13
14
|
|
15
|
+
|
14
16
|
def convert_tool_to_langchain_tool(
|
15
17
|
tool: Tool,
|
16
18
|
):
|
17
19
|
from langchain_core.tools import StructuredTool
|
20
|
+
|
18
21
|
"""Convert an tool to a LangChain tool.
|
19
22
|
|
20
23
|
NOTE: this tool can be executed only in a context of an active MCP client session.
|
@@ -37,4 +40,4 @@ def convert_tool_to_langchain_tool(
|
|
37
40
|
description=tool.description or "",
|
38
41
|
coroutine=call_tool,
|
39
42
|
response_format="content",
|
40
|
-
)
|
43
|
+
)
|
@@ -135,12 +135,9 @@ class FuncMetadata(BaseModel):
|
|
135
135
|
arbitrary_types_allowed=True,
|
136
136
|
)
|
137
137
|
|
138
|
-
|
139
138
|
@classmethod
|
140
139
|
def func_metadata(
|
141
|
-
cls,
|
142
|
-
func: Callable[..., Any],
|
143
|
-
skip_names: Sequence[str] = ()
|
140
|
+
cls, func: Callable[..., Any], skip_names: Sequence[str] = ()
|
144
141
|
) -> "FuncMetadata":
|
145
142
|
"""Given a function, return metadata including a pydantic model representing its
|
146
143
|
signature.
|
@@ -201,7 +198,10 @@ class FuncMetadata(BaseModel):
|
|
201
198
|
if param.default is not inspect.Parameter.empty
|
202
199
|
else PydanticUndefined,
|
203
200
|
)
|
204
|
-
dynamic_pydantic_model_params[param.name] = (
|
201
|
+
dynamic_pydantic_model_params[param.name] = (
|
202
|
+
field_info.annotation,
|
203
|
+
field_info,
|
204
|
+
)
|
205
205
|
continue
|
206
206
|
|
207
207
|
arguments_model = create_model(
|
@@ -211,4 +211,3 @@ class FuncMetadata(BaseModel):
|
|
211
211
|
)
|
212
212
|
resp = FuncMetadata(arg_model=arguments_model)
|
213
213
|
return resp
|
214
|
-
|