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
universal_mcp/servers/server.py
CHANGED
@@ -47,14 +47,20 @@ class Server(FastMCP, ABC):
|
|
47
47
|
|
48
48
|
async def call_tool(self, name: str, arguments: dict[str, Any]):
|
49
49
|
"""Call a tool by name with arguments."""
|
50
|
+
logger.info(f"Calling tool: {name} with arguments: {arguments}")
|
50
51
|
try:
|
51
52
|
result = await super().call_tool(name, arguments)
|
53
|
+
logger.info(f"Tool {name} completed successfully")
|
52
54
|
return result
|
53
55
|
except ToolError as e:
|
54
56
|
raised_error = e.__cause__
|
55
57
|
if isinstance(raised_error, NotAuthorizedError):
|
58
|
+
logger.warning(
|
59
|
+
f"Not authorized to call tool {name}: {raised_error.message}"
|
60
|
+
)
|
56
61
|
return [TextContent(type="text", text=raised_error.message)]
|
57
62
|
else:
|
63
|
+
logger.error(f"Error calling tool {name}: {str(e)}")
|
58
64
|
raise e
|
59
65
|
|
60
66
|
|
@@ -101,22 +107,18 @@ class LocalServer(Server):
|
|
101
107
|
def _load_apps(self):
|
102
108
|
logger.info(f"Loading apps: {self.apps_list}")
|
103
109
|
for app_config in self.apps_list:
|
104
|
-
|
105
|
-
|
106
|
-
|
110
|
+
try:
|
111
|
+
app = self._load_app(app_config)
|
112
|
+
if app:
|
113
|
+
tools = app.list_tools()
|
107
114
|
for tool in tools:
|
108
|
-
|
115
|
+
tool_name = tool.__name__
|
116
|
+
name = app.name + "_" + tool_name
|
109
117
|
description = tool.__doc__
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
):
|
115
|
-
should_add_tool = True
|
116
|
-
if should_add_tool:
|
117
|
-
self.add_tool(
|
118
|
-
tool, name=full_tool_name, description=description
|
119
|
-
)
|
118
|
+
if app_config.actions is None or tool_name in app_config.actions:
|
119
|
+
self.add_tool(tool, name=name, description=description)
|
120
|
+
except Exception as e:
|
121
|
+
logger.error(f"Error loading app {app_config.name}: {e}")
|
120
122
|
|
121
123
|
|
122
124
|
class AgentRServer(Server):
|
@@ -143,7 +145,7 @@ class AgentRServer(Server):
|
|
143
145
|
app = app_from_slug(name)(integration=integration)
|
144
146
|
return app
|
145
147
|
|
146
|
-
def _list_apps_with_integrations(self):
|
148
|
+
def _list_apps_with_integrations(self) -> list[AppConfig]:
|
147
149
|
# TODO: get this from the API
|
148
150
|
response = httpx.get(
|
149
151
|
f"{self.base_url}/api/apps/", headers={"X-API-KEY": self.api_key}
|
@@ -156,11 +158,16 @@ class AgentRServer(Server):
|
|
156
158
|
|
157
159
|
def _load_apps(self):
|
158
160
|
apps = self._list_apps_with_integrations()
|
159
|
-
for
|
160
|
-
|
161
|
-
|
162
|
-
|
161
|
+
for app_config in apps:
|
162
|
+
try:
|
163
|
+
app = self._load_app(app_config)
|
164
|
+
if app:
|
165
|
+
tools = app.list_tools()
|
163
166
|
for tool in tools:
|
164
|
-
|
167
|
+
tool_name = tool.__name__
|
168
|
+
name = app.name + "_" + tool_name
|
165
169
|
description = tool.__doc__
|
166
|
-
|
170
|
+
if app_config.actions is None or tool_name in app_config.actions:
|
171
|
+
self.add_tool(tool, name=name, description=description)
|
172
|
+
except Exception as e:
|
173
|
+
logger.error(f"Error loading app {app_config.name}: {e}")
|
universal_mcp/utils/docgen.py
CHANGED
@@ -116,7 +116,7 @@ def extract_functions_from_script(file_path: str) -> list[tuple[str, str]]:
|
|
116
116
|
|
117
117
|
|
118
118
|
def generate_docstring(
|
119
|
-
function_code: str, model: str = "
|
119
|
+
function_code: str, model: str = "openai/gpt-4o"
|
120
120
|
) -> DocstringOutput:
|
121
121
|
"""
|
122
122
|
Generate a docstring for a Python function using litellm with structured output.
|
@@ -304,7 +304,7 @@ def insert_docstring_into_function(function_code: str, docstring: str) -> str:
|
|
304
304
|
return function_code
|
305
305
|
|
306
306
|
|
307
|
-
def process_file(file_path: str, model: str = "
|
307
|
+
def process_file(file_path: str, model: str = "openai/gpt-4o") -> int:
|
308
308
|
"""
|
309
309
|
Process a Python file and add docstrings to all functions in it.
|
310
310
|
|
@@ -4,6 +4,7 @@ import sys
|
|
4
4
|
from pathlib import Path
|
5
5
|
|
6
6
|
from loguru import logger
|
7
|
+
from rich import print
|
7
8
|
|
8
9
|
|
9
10
|
def get_uvx_path() -> str:
|
@@ -21,6 +22,7 @@ def get_uvx_path() -> str:
|
|
21
22
|
def create_file_if_not_exists(path: Path) -> None:
|
22
23
|
"""Create a file if it doesn't exist"""
|
23
24
|
if not path.exists():
|
25
|
+
print(f"[yellow]Creating config file at {path}[/yellow]")
|
24
26
|
with open(path, "w") as f:
|
25
27
|
json.dump({}, f)
|
26
28
|
|
@@ -32,6 +34,7 @@ def get_supported_apps() -> list[str]:
|
|
32
34
|
|
33
35
|
def install_claude(api_key: str) -> None:
|
34
36
|
"""Install Claude"""
|
37
|
+
print("[bold blue]Installing Claude configuration...[/bold blue]")
|
35
38
|
# Determine platform-specific config path
|
36
39
|
if sys.platform == "darwin": # macOS
|
37
40
|
config_path = (
|
@@ -50,6 +53,9 @@ def install_claude(api_key: str) -> None:
|
|
50
53
|
try:
|
51
54
|
config = json.loads(config_path.read_text())
|
52
55
|
except json.JSONDecodeError:
|
56
|
+
print(
|
57
|
+
"[yellow]Config file was empty or invalid, creating new configuration[/yellow]"
|
58
|
+
)
|
53
59
|
config = {}
|
54
60
|
if "mcpServers" not in config:
|
55
61
|
config["mcpServers"] = {}
|
@@ -60,10 +66,12 @@ def install_claude(api_key: str) -> None:
|
|
60
66
|
}
|
61
67
|
with open(config_path, "w") as f:
|
62
68
|
json.dump(config, f, indent=4)
|
69
|
+
print("[green]✓[/green] Claude configuration installed successfully")
|
63
70
|
|
64
71
|
|
65
72
|
def install_cursor(api_key: str) -> None:
|
66
73
|
"""Install Cursor"""
|
74
|
+
print("[bold blue]Installing Cursor configuration...[/bold blue]")
|
67
75
|
# Set up Cursor config path
|
68
76
|
config_path = Path.home() / ".cursor/mcp.json"
|
69
77
|
|
@@ -73,6 +81,9 @@ def install_cursor(api_key: str) -> None:
|
|
73
81
|
try:
|
74
82
|
config = json.loads(config_path.read_text())
|
75
83
|
except json.JSONDecodeError:
|
84
|
+
print(
|
85
|
+
"[yellow]Config file was empty or invalid, creating new configuration[/yellow]"
|
86
|
+
)
|
76
87
|
config = {}
|
77
88
|
|
78
89
|
if "mcpServers" not in config:
|
@@ -85,15 +96,18 @@ def install_cursor(api_key: str) -> None:
|
|
85
96
|
|
86
97
|
with open(config_path, "w") as f:
|
87
98
|
json.dump(config, f, indent=4)
|
99
|
+
print("[green]✓[/green] Cursor configuration installed successfully")
|
88
100
|
|
89
101
|
|
90
102
|
def install_windsurf() -> None:
|
91
103
|
"""Install Windsurf"""
|
104
|
+
print("[yellow]Windsurf installation not yet implemented[/yellow]")
|
92
105
|
pass
|
93
106
|
|
94
107
|
|
95
108
|
def install_app(app_name: str) -> None:
|
96
109
|
"""Install an app"""
|
110
|
+
print(f"[bold]Installing {app_name}...[/bold]")
|
97
111
|
if app_name == "claude":
|
98
112
|
install_claude()
|
99
113
|
elif app_name == "cursor":
|
@@ -101,4 +115,5 @@ def install_app(app_name: str) -> None:
|
|
101
115
|
elif app_name == "windsurf":
|
102
116
|
install_windsurf()
|
103
117
|
else:
|
118
|
+
print(f"[red]Error: App '{app_name}' not supported[/red]")
|
104
119
|
raise ValueError(f"App '{app_name}' not supported")
|
universal_mcp/utils/openapi.py
CHANGED
@@ -41,7 +41,7 @@ def determine_return_type(operation: dict[str, Any]) -> str:
|
|
41
41
|
operation (dict): The operation details from the schema.
|
42
42
|
|
43
43
|
Returns:
|
44
|
-
str: The appropriate return type annotation (
|
44
|
+
str: The appropriate return type annotation (list[Any], dict[str, Any], or Any)
|
45
45
|
"""
|
46
46
|
responses = operation.get("responses", {})
|
47
47
|
# Find successful response (2XX)
|
@@ -62,9 +62,9 @@ def determine_return_type(operation: dict[str, Any]) -> str:
|
|
62
62
|
|
63
63
|
# Only determine if it's a list, dict, or unknown (Any)
|
64
64
|
if schema.get("type") == "array":
|
65
|
-
return "
|
65
|
+
return "list[Any]"
|
66
66
|
elif schema.get("type") == "object" or "$ref" in schema:
|
67
|
-
return "
|
67
|
+
return "dict[str, Any]"
|
68
68
|
|
69
69
|
# Default to Any if unable to determine
|
70
70
|
return "Any"
|
@@ -144,9 +144,9 @@ def generate_api_client(schema):
|
|
144
144
|
|
145
145
|
# Generate class imports
|
146
146
|
imports = [
|
147
|
+
"from typing import Any",
|
147
148
|
"from universal_mcp.applications import APIApplication",
|
148
149
|
"from universal_mcp.integrations import Integration",
|
149
|
-
"from typing import Any, Dict, List",
|
150
150
|
]
|
151
151
|
|
152
152
|
# Construct the class code
|
@@ -177,6 +177,9 @@ def generate_method_code(path, method, operation, tool_name=None):
|
|
177
177
|
Returns:
|
178
178
|
tuple: (method_code, func_name) - The Python code for the method and its name.
|
179
179
|
"""
|
180
|
+
# Extract path parameters from the URL path
|
181
|
+
path_params_in_url = re.findall(r'{([^}]+)}', path)
|
182
|
+
|
180
183
|
# Determine function name
|
181
184
|
if "operationId" in operation:
|
182
185
|
raw_name = operation["operationId"]
|
@@ -192,24 +195,35 @@ def generate_method_code(path, method, operation, tool_name=None):
|
|
192
195
|
else:
|
193
196
|
name_parts.append(part)
|
194
197
|
func_name = "_".join(name_parts).replace("-", "_").lower()
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
198
|
+
|
199
|
+
|
200
|
+
|
201
|
+
func_name = re.sub(r'_a([^_])', r'_a_\1', func_name) # Fix for patterns like retrieve_ablock
|
202
|
+
func_name = re.sub(r'_an([^_])', r'_an_\1', func_name) # Fix for patterns like create_anitem
|
199
203
|
|
200
204
|
# Get parameters and request body
|
201
|
-
|
205
|
+
# Filter out header parameters
|
206
|
+
parameters = [param for param in operation.get("parameters", []) if param.get("in") != "header"]
|
202
207
|
has_body = "requestBody" in operation
|
203
208
|
body_required = has_body and operation["requestBody"].get("required", False)
|
204
209
|
|
205
210
|
# Build function arguments
|
206
211
|
required_args = []
|
207
212
|
optional_args = []
|
213
|
+
|
214
|
+
|
215
|
+
for param_name in path_params_in_url:
|
216
|
+
if param_name not in required_args:
|
217
|
+
required_args.append(param_name)
|
218
|
+
|
219
|
+
|
208
220
|
for param in parameters:
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
221
|
+
param_name = param["name"]
|
222
|
+
if param_name not in required_args:
|
223
|
+
if param.get("required", False):
|
224
|
+
required_args.append(param_name)
|
225
|
+
else:
|
226
|
+
optional_args.append(f"{param_name}=None")
|
213
227
|
|
214
228
|
# Add request body parameter
|
215
229
|
if has_body:
|
@@ -229,12 +243,12 @@ def generate_method_code(path, method, operation, tool_name=None):
|
|
229
243
|
# Build method body
|
230
244
|
body_lines = []
|
231
245
|
|
232
|
-
# Validate required parameters
|
233
|
-
for
|
234
|
-
if
|
235
|
-
body_lines.append(f" if {
|
246
|
+
# Validate required parameters including path parameters
|
247
|
+
for param_name in required_args:
|
248
|
+
if param_name != "request_body": # Skip validation for request body as it's handled separately
|
249
|
+
body_lines.append(f" if {param_name} is None:")
|
236
250
|
body_lines.append(
|
237
|
-
f" raise ValueError(\"Missing required parameter '{
|
251
|
+
f" raise ValueError(\"Missing required parameter '{param_name}'\")"
|
238
252
|
)
|
239
253
|
|
240
254
|
# Validate required body
|
@@ -244,15 +258,9 @@ def generate_method_code(path, method, operation, tool_name=None):
|
|
244
258
|
' raise ValueError("Missing required request body")'
|
245
259
|
)
|
246
260
|
|
247
|
-
#
|
248
|
-
|
249
|
-
|
250
|
-
body_lines.append(f" path_params = {{{path_params_dict}}}")
|
251
|
-
|
252
|
-
# Format URL
|
253
|
-
body_lines.append(
|
254
|
-
f' url = f"{{self.base_url}}{path}".format_map(path_params)'
|
255
|
-
)
|
261
|
+
# Format URL directly with path parameters
|
262
|
+
url_line = f' url = f"{{self.base_url}}{path}"'
|
263
|
+
body_lines.append(url_line)
|
256
264
|
|
257
265
|
# Query parameters
|
258
266
|
query_params = [p for p in parameters if p["in"] == "query"]
|
@@ -266,40 +274,32 @@ def generate_method_code(path, method, operation, tool_name=None):
|
|
266
274
|
else:
|
267
275
|
body_lines.append(" query_params = {}")
|
268
276
|
|
269
|
-
# Request body handling for JSON
|
270
|
-
if has_body:
|
271
|
-
body_lines.append(
|
272
|
-
" json_body = request_body if request_body is not None else None"
|
273
|
-
)
|
274
|
-
|
275
277
|
# Make HTTP request using the proper method
|
276
278
|
method_lower = method.lower()
|
277
279
|
if method_lower == "get":
|
278
280
|
body_lines.append(" response = self._get(url, params=query_params)")
|
279
281
|
elif method_lower == "post":
|
280
282
|
if has_body:
|
281
|
-
body_lines.append(
|
282
|
-
" response = self._post(url, data=json_body, params=query_params)"
|
283
|
-
)
|
283
|
+
body_lines.append(" response = self._post(url, data=request_body, params=query_params)")
|
284
284
|
else:
|
285
|
-
body_lines.append(
|
286
|
-
" response = self._post(url, data={}, params=query_params)"
|
287
|
-
)
|
285
|
+
body_lines.append(" response = self._post(url, data={}, params=query_params)")
|
288
286
|
elif method_lower == "put":
|
289
287
|
if has_body:
|
290
|
-
body_lines.append(
|
291
|
-
" response = self._put(url, data=json_body, params=query_params)"
|
292
|
-
)
|
288
|
+
body_lines.append(" response = self._put(url, data=request_body, params=query_params)")
|
293
289
|
else:
|
294
|
-
body_lines.append(
|
295
|
-
|
296
|
-
|
290
|
+
body_lines.append(" response = self._put(url, data={}, params=query_params)")
|
291
|
+
elif method_lower == "patch":
|
292
|
+
if has_body:
|
293
|
+
body_lines.append(" response = self._patch(url, data=request_body, params=query_params)")
|
294
|
+
else:
|
295
|
+
body_lines.append(" response = self._patch(url, data={}, params=query_params)")
|
297
296
|
elif method_lower == "delete":
|
298
297
|
body_lines.append(" response = self._delete(url, params=query_params)")
|
299
298
|
else:
|
300
|
-
|
301
|
-
f" response = self._{method_lower}(url, data=
|
302
|
-
|
299
|
+
if has_body:
|
300
|
+
body_lines.append(f" response = self._{method_lower}(url, data=request_body, params=query_params)")
|
301
|
+
else:
|
302
|
+
body_lines.append(f" response = self._{method_lower}(url, data={{}}, params=query_params)")
|
303
303
|
|
304
304
|
# Handle response
|
305
305
|
body_lines.append(" response.raise_for_status()")
|
@@ -0,0 +1,252 @@
|
|
1
|
+
Metadata-Version: 2.4
|
2
|
+
Name: universal-mcp
|
3
|
+
Version: 0.1.3rc1
|
4
|
+
Summary: Universal MCP acts as a middle ware for your API applications. It can store your credentials, authorize, enable disable apps on the fly and much more.
|
5
|
+
Author-email: Manoj Bajaj <manojbajaj95@gmail.com>
|
6
|
+
Requires-Python: >=3.11
|
7
|
+
Requires-Dist: keyring>=25.6.0
|
8
|
+
Requires-Dist: litellm>=1.30.7
|
9
|
+
Requires-Dist: loguru>=0.7.3
|
10
|
+
Requires-Dist: markitdown[all]>=0.1.1
|
11
|
+
Requires-Dist: mcp>=1.5.0
|
12
|
+
Requires-Dist: posthog>=3.24.0
|
13
|
+
Requires-Dist: pydantic-settings>=2.8.1
|
14
|
+
Requires-Dist: pydantic>=2.11.1
|
15
|
+
Requires-Dist: pyyaml>=6.0.2
|
16
|
+
Requires-Dist: rich>=14.0.0
|
17
|
+
Requires-Dist: typer>=0.15.2
|
18
|
+
Provides-Extra: dev
|
19
|
+
Requires-Dist: litellm>=1.30.7; extra == 'dev'
|
20
|
+
Requires-Dist: pyright>=1.1.398; extra == 'dev'
|
21
|
+
Requires-Dist: pytest-asyncio>=0.26.0; extra == 'dev'
|
22
|
+
Requires-Dist: pytest>=8.3.5; extra == 'dev'
|
23
|
+
Requires-Dist: ruff>=0.11.4; extra == 'dev'
|
24
|
+
Provides-Extra: e2b
|
25
|
+
Requires-Dist: e2b-code-interpreter>=1.2.0; extra == 'e2b'
|
26
|
+
Provides-Extra: firecrawl
|
27
|
+
Requires-Dist: firecrawl-py>=1.15.0; extra == 'firecrawl'
|
28
|
+
Provides-Extra: playground
|
29
|
+
Requires-Dist: fastapi[standard]>=0.115.12; extra == 'playground'
|
30
|
+
Requires-Dist: langchain-anthropic>=0.3.10; extra == 'playground'
|
31
|
+
Requires-Dist: langchain-mcp-adapters>=0.0.3; extra == 'playground'
|
32
|
+
Requires-Dist: langchain-openai>=0.3.12; extra == 'playground'
|
33
|
+
Requires-Dist: langgraph-checkpoint-sqlite>=2.0.6; extra == 'playground'
|
34
|
+
Requires-Dist: langgraph>=0.3.24; extra == 'playground'
|
35
|
+
Requires-Dist: python-dotenv>=1.0.1; extra == 'playground'
|
36
|
+
Requires-Dist: streamlit>=1.44.1; extra == 'playground'
|
37
|
+
Provides-Extra: serpapi
|
38
|
+
Requires-Dist: google-search-results>=2.4.2; extra == 'serpapi'
|
39
|
+
Description-Content-Type: text/markdown
|
40
|
+
|
41
|
+
# Universal MCP
|
42
|
+
Universal MCP acts as a middleware layer for your API applications, enabling seamless integration with various services through the Model Context Protocol (MCP). It simplifies credential management, authorization, and dynamic app enablement.
|
43
|
+
|
44
|
+
## 🌟 Features
|
45
|
+
|
46
|
+
- **MCP (Model Context Protocol) Integration**: Seamlessly works with MCP server architecture
|
47
|
+
- **Simplified API Integration**: Connect to services like GitHub, Google Calendar, Gmail, Reddit, Tavily, and more with minimal code
|
48
|
+
- **Managed Authentication**: Built-in support for API keys and OAuth-based authentication flows
|
49
|
+
- **Extensible Architecture**: Easily build and add new app integrations with minimal boilerplate
|
50
|
+
- **Credential Management**: Flexible storage options for API credentials with memory and environment-based implementations
|
51
|
+
|
52
|
+
## 🔧 Installation
|
53
|
+
|
54
|
+
Install Universal MCP using pip:
|
55
|
+
|
56
|
+
```bash
|
57
|
+
pip install universal-mcp
|
58
|
+
```
|
59
|
+
|
60
|
+
## 🚀 Quick Start
|
61
|
+
|
62
|
+
**Important Prerequisite: AgentR API Key (If Using AgentR Integration)**
|
63
|
+
|
64
|
+
If you plan to use integrations with `type: "agentr"` (for services like GitHub, Gmail, Notion via the AgentR platform), or if you run the server with `type: "agentr"`, you first need an AgentR API key:
|
65
|
+
|
66
|
+
1. Visit [https://agentr.dev](https://agentr.dev) to create an account and generate an API key from your dashboard.
|
67
|
+
2. Set it as an environment variable *before* running the MCP server:
|
68
|
+
```bash
|
69
|
+
export AGENTR_API_KEY="your_api_key_here"
|
70
|
+
```
|
71
|
+
|
72
|
+
**1. Create a Configuration File (e.g., `config.json`)**
|
73
|
+
|
74
|
+
This file defines the server settings, credential stores, and the applications to load with their respective integrations.
|
75
|
+
|
76
|
+
```python
|
77
|
+
{
|
78
|
+
"name": "My Local MCP Server",
|
79
|
+
"description": "A server for testing applications locally",
|
80
|
+
"type": "local", # Or "agentr" to load apps dynamically from AgentR
|
81
|
+
"transport": "sse",
|
82
|
+
"port": 8005,
|
83
|
+
"store": {
|
84
|
+
"name": "my_mcp_store",
|
85
|
+
"type": "keyring"
|
86
|
+
},
|
87
|
+
"apps": [
|
88
|
+
{
|
89
|
+
"name": "zenquotes", # App slug
|
90
|
+
"integration": null # No authentication needed
|
91
|
+
},
|
92
|
+
{
|
93
|
+
"name": "tavily",
|
94
|
+
"integration": {
|
95
|
+
"name": "TAVILY_API_KEY", # Unique name for this credential
|
96
|
+
"type": "api_key",
|
97
|
+
"store": {
|
98
|
+
"type": "environment"
|
99
|
+
}
|
100
|
+
}
|
101
|
+
},
|
102
|
+
{
|
103
|
+
"name": "github",
|
104
|
+
"integration": {
|
105
|
+
"name": "github", # Matches the service name in AgentR
|
106
|
+
"type": "agentr" # Uses AgentR platform for auth/creds
|
107
|
+
}
|
108
|
+
}
|
109
|
+
]
|
110
|
+
}
|
111
|
+
```
|
112
|
+
|
113
|
+
*Notes:*
|
114
|
+
* `type: "local"` runs applications defined directly in the config's `apps` list.
|
115
|
+
* `type: "agentr"` connects to the AgentR platform to dynamically load user-enabled apps (ignores the `apps` list in the config) and handle credentials (requires `AGENTR_API_KEY` env var).
|
116
|
+
* `store`: Defines credential storage. `environment` looks for `<INTEGRATION_NAME_UPPERCASE>` env var (e.g., `TAVILY_API_KEY`). `keyring` uses the system's secure storage. `memory` is transient.
|
117
|
+
* `integration`: Configures authentication for each app when using `type: "local"`. `type: "agentr"` uses the AgentR platform for OAuth/credential management. `type: "api_key"` uses the specified `store`.
|
118
|
+
|
119
|
+
**2. Run the Server via CLI**
|
120
|
+
|
121
|
+
Make sure any required environment variables (like `TAVILY_API_KEY` for the example above, or `AGENTR_API_KEY` if using `"agentr"` type server/integrations) are set.
|
122
|
+
|
123
|
+
```bash
|
124
|
+
universal_mcp run -c config.json
|
125
|
+
```
|
126
|
+
|
127
|
+
The server will start, load the configured applications (or connect to AgentR if `type: "agentr"`), and listen for connections based on the `transport` type (`sse`, `stdio`, or `http`).
|
128
|
+
|
129
|
+
## 🛠️ Using Playground
|
130
|
+
|
131
|
+
The `playground` directory provides a runnable example with a FastAPI backend and a Streamlit frontend for interacting with the MCP server.
|
132
|
+
|
133
|
+
**Prerequisites:**
|
134
|
+
|
135
|
+
* Ensure `local_config.json` exists in the project root directory. See `src/playground/README.md` for its format. This configures the *local* MCP server that the playground backend connects to.
|
136
|
+
* Install playground dependencies if needed (e.g., `fastapi`, `streamlit`, `uvicorn`, `langchain`, etc.).
|
137
|
+
|
138
|
+
**Running the Playground:**
|
139
|
+
|
140
|
+
The easiest way is to use the automated startup script from the **project root directory**:
|
141
|
+
|
142
|
+
```bash
|
143
|
+
python src/playground
|
144
|
+
```
|
145
|
+
Refer to `src/playground/README.md` for more detailed setup and usage instructions.
|
146
|
+
|
147
|
+
## 🧩 Available Applications
|
148
|
+
|
149
|
+
Universal MCP comes with several pre-built applications:
|
150
|
+
|
151
|
+
| Application Slug | Description | Authentication Type |
|
152
|
+
| :--------------- | :--------------------------------------- | :---------------------------------------- |
|
153
|
+
| `e2b` | Execute Python code in secure sandboxes | API Key (via Integration) |
|
154
|
+
| `firecrawl` | Scrape/crawl web pages, search | API Key (via Integration) |
|
155
|
+
| `github` | Interact with GitHub repos, issues, PRs | OAuth (AgentR) |
|
156
|
+
| `google-calendar`| Manage Google Calendar events | OAuth (AgentR) |
|
157
|
+
| `google-mail` | Read and send Gmail emails | OAuth (AgentR) |
|
158
|
+
| `markitdown` | Convert web pages/files to Markdown | None |
|
159
|
+
| `notion` | Interact with Notion pages/databases | OAuth (AgentR) |
|
160
|
+
| `reddit` | Interact with Reddit posts/comments | OAuth (AgentR) |
|
161
|
+
| `resend` | Send emails via Resend API | API Key (via Integration) |
|
162
|
+
| `serp` | Perform web searches via SerpApi | API Key (via Integration) |
|
163
|
+
| `tavily` | Advanced web search & research API | API Key (via Integration) |
|
164
|
+
| `zenquotes` | Get inspirational quotes | None |
|
165
|
+
|
166
|
+
*Authentication Type notes:*
|
167
|
+
* *OAuth (AgentR)*: Typically requires configuring the integration with `type: "agentr"` in your `ServerConfig`. Requires the `AGENTR_API_KEY`.
|
168
|
+
* *API Key (via Integration)*: Requires configuring `type: "api_key"` and a `store` (like `environment` or `keyring`) in your `ServerConfig`.
|
169
|
+
|
170
|
+
## 🔐 Integration Types
|
171
|
+
|
172
|
+
Universal MCP supports different ways to handle authentication for applications:
|
173
|
+
|
174
|
+
### 1. API Key Integration
|
175
|
+
|
176
|
+
For services that authenticate via simple API keys. Configure using `IntegrationConfig` with `type: "api_key"`.
|
177
|
+
|
178
|
+
```python
|
179
|
+
{
|
180
|
+
"name": "tavily",
|
181
|
+
"integration": {
|
182
|
+
"name": "TAVILY_API_KEY",
|
183
|
+
"type": "api_key",
|
184
|
+
"store": {
|
185
|
+
"name": "universal_mcp",
|
186
|
+
"type": "environment" # Or "keyring", "memory"
|
187
|
+
}
|
188
|
+
}
|
189
|
+
}
|
190
|
+
```
|
191
|
+
|
192
|
+
### 2. AgentR Integration
|
193
|
+
|
194
|
+
For services integrated with the AgentR platform, typically handling OAuth flows or centrally managed credentials. Configure using `IntegrationConfig` with `type: "agentr"`. Requires the `AGENTR_API_KEY` environment variable to be set for the MCP server process.
|
195
|
+
|
196
|
+
```python
|
197
|
+
{
|
198
|
+
"name": "github",
|
199
|
+
"integration": {
|
200
|
+
"name": "github", # Matches the service name configured in AgentR
|
201
|
+
"type": "agentr"
|
202
|
+
}
|
203
|
+
}
|
204
|
+
```
|
205
|
+
When an action requiring authorization is called, the `AgentRIntegration` will prompt the user (via the MCP client) to visit a URL to complete the OAuth flow managed by AgentR. This is also the default integration type when using `type: "agentr"` for the main server config.
|
206
|
+
|
207
|
+
### 3. OAuth Integration (Direct - Less Common)
|
208
|
+
|
209
|
+
While `AgentRIntegration` is preferred for OAuth, a direct `OAuthIntegration` class exists but requires manual configuration of client IDs, secrets, and handling callbacks, which is generally more complex to set up outside the AgentR platform.
|
210
|
+
|
211
|
+
## 🤖 CLI Usage
|
212
|
+
|
213
|
+
Universal MCP includes a command-line interface:
|
214
|
+
|
215
|
+
```bash
|
216
|
+
# Run the MCP server using a configuration file
|
217
|
+
universal_mcp run -c config.json
|
218
|
+
|
219
|
+
# Generate API client code and application structure from an OpenAPI schema
|
220
|
+
# Output file name (e.g., 'twitter.py') determines app name ('twitter')
|
221
|
+
universal_mcp generate --schema <path_to_schema.json/yaml> --output <path/to/output_app_name.py> [--no-docstrings]
|
222
|
+
|
223
|
+
# Generate Google-style docstrings for functions in a Python file using an LLM
|
224
|
+
universal_mcp docgen <path/to/file.py> [--model <model_name>] [--api-key <llm_api_key>]
|
225
|
+
|
226
|
+
# Install MCP configuration for supported desktop apps (Claude, Cursor)
|
227
|
+
# Requires AgentR API key for configuration.
|
228
|
+
universal_mcp install claude
|
229
|
+
universal_mcp install cursor
|
230
|
+
|
231
|
+
# Check installed version (standard typer command)
|
232
|
+
universal_mcp --version
|
233
|
+
```
|
234
|
+
|
235
|
+
## 📋 Requirements
|
236
|
+
|
237
|
+
- Python 3.11+
|
238
|
+
- Dependencies (installed automatically via pip):
|
239
|
+
- `mcp-server`
|
240
|
+
- `loguru`
|
241
|
+
- `typer`
|
242
|
+
- `httpx`
|
243
|
+
- `pydantic`
|
244
|
+
- `pyyaml`
|
245
|
+
- `keyring` (optional, for `KeyringStore`)
|
246
|
+
- `litellm` (optional, for `docgen` command)
|
247
|
+
- ... and others specific to certain applications.
|
248
|
+
|
249
|
+
## 📝 License
|
250
|
+
|
251
|
+
This project is licensed under the MIT License.
|
252
|
+
|
@@ -1,17 +1,22 @@
|
|
1
1
|
universal_mcp/__init__.py,sha256=2gdHpHaDDcsRjZjJ01FLN-1iidN_wbDAolNpxhGoFB4,59
|
2
|
-
universal_mcp/cli.py,sha256=
|
2
|
+
universal_mcp/cli.py,sha256=DG-Qxc5vQIdbhAIQuU7bKKJuRGzwyOigjfCKSWBRhBI,5258
|
3
3
|
universal_mcp/config.py,sha256=9eb3DDg4PBBr1MlGeBrA4bja3Y6howOH-UKpo7JIbs8,828
|
4
4
|
universal_mcp/exceptions.py,sha256=Zp2_v_m3L7GDAmD1ZyuwFtY6ngapdhxuIygrvpZAQtM,271
|
5
|
+
universal_mcp/logger.py,sha256=KFRYc96RcyqiDVB8i0lTlVrHmXXB_C8on2QeZgrqBrw,2058
|
5
6
|
universal_mcp/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
6
7
|
universal_mcp/applications/__init__.py,sha256=qeWnbdIudyMR7ST4XTc0gpEM9o6TsM1ZnZ92dMAPSBA,754
|
7
8
|
universal_mcp/applications/application.py,sha256=dqp8lgIi2xhY62imwo7C6769URQtNmqd6Ok6PiTr6wc,3399
|
8
9
|
universal_mcp/applications/e2b/app.py,sha256=5piCipi1TC_KuKLFP17do1Y1r28fqApvRsZy76equ9w,2573
|
9
|
-
universal_mcp/applications/firecrawl/app.py,sha256=
|
10
|
+
universal_mcp/applications/firecrawl/app.py,sha256=RSy8zRn4k1A1tIpJNqrUnPI8ctEv1nKWuOuJQcp9mGo,9264
|
10
11
|
universal_mcp/applications/github/README.md,sha256=m_6FlPpx9l5y29TkFMewCXMXSRHriDtj71qyYYeFzCw,643
|
11
12
|
universal_mcp/applications/github/app.py,sha256=L201f5MSx1YVx0nqgduZ5gyHPZdX0UfcEhPmDWiWK6s,13686
|
12
13
|
universal_mcp/applications/google_calendar/app.py,sha256=g_3vrsM2ltwpTySgC5I4SYg47n4UJiYigECO0ax1EHM,19134
|
13
14
|
universal_mcp/applications/google_mail/app.py,sha256=VXeD3_TRgYIUDFUzDPCKgR47XvovxLFulD-HG22hls8,22716
|
14
|
-
universal_mcp/applications/markitdown/app.py,sha256=
|
15
|
+
universal_mcp/applications/markitdown/app.py,sha256=Gh12f1dW6DA_AW5DuStbCkOR7KtyJp8VEjdTaIicrRI,1996
|
16
|
+
universal_mcp/applications/notion/README.md,sha256=45NmPOmSQv99qBvWdwmnV5vbaYc9_8vq8I-FA7veVAA,2600
|
17
|
+
universal_mcp/applications/notion/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
18
|
+
universal_mcp/applications/notion/app.py,sha256=XpLnmeXj0Gnf_RYHlbAFnwtSCTYsrNzk6MSMSyDmHGQ,17283
|
19
|
+
universal_mcp/applications/perplexity/app.py,sha256=J27IDcz9pRC_uW4wABpn-EcI4RvKIzoh_o2hzysM5wA,3187
|
15
20
|
universal_mcp/applications/reddit/app.py,sha256=leU__w5VxX1vMK-kfuy-dvY97Pn8Mn80X2payVshirU,13562
|
16
21
|
universal_mcp/applications/resend/app.py,sha256=bRo-CRDuk65EUSHOJnbVHWV6TuiUHtedz6FXKRS1ym0,1386
|
17
22
|
universal_mcp/applications/serp/app.py,sha256=hPXu1sBiRZRCCzr4q2uvt54F0-B3aZK2Uz4wfKokkZ4,3131
|
@@ -20,18 +25,18 @@ universal_mcp/applications/zenquotes/app.py,sha256=nidRGwVORIU25QGCCbjDIv1UNFUj5
|
|
20
25
|
universal_mcp/integrations/README.md,sha256=lTAPXO2nivcBe1q7JT6PRa6v9Ns_ZersQMIdw-nmwEA,996
|
21
26
|
universal_mcp/integrations/__init__.py,sha256=8e11JZyctaR9CmlNkfEZ6HhGDvhlvf9iug2wdjb5pwY,270
|
22
27
|
universal_mcp/integrations/agentr.py,sha256=l0mo79oeDML19udFfoCo9lyhbDAf0X94_lnpOgbTrb0,3331
|
23
|
-
universal_mcp/integrations/integration.py,sha256=
|
28
|
+
universal_mcp/integrations/integration.py,sha256=8TYr7N1F6oV8PPOSLo0eA_6nD-vrwIWbuplqwD03uuQ,5819
|
24
29
|
universal_mcp/servers/__init__.py,sha256=dgRW_khG537GeLKC5_U5jhxCuu1L_1YeTujeDg0601E,654
|
25
|
-
universal_mcp/servers/server.py,sha256=
|
30
|
+
universal_mcp/servers/server.py,sha256=uHArnw_kYIhvlE-Zp5GharGqapGy4jaOlxRQGsOafPM,6431
|
26
31
|
universal_mcp/stores/__init__.py,sha256=Sc4AWtee_qtK5hpEVUAH2XM_6EBhcfikQXWiGXdNfes,560
|
27
32
|
universal_mcp/stores/store.py,sha256=CNOnmKeOCkSU2ZA9t12AIWJcmqZZX_LSyZaV8FQf8Xk,4545
|
28
33
|
universal_mcp/utils/__init__.py,sha256=8wi4PGWu-SrFjNJ8U7fr2iFJ1ktqlDmSKj1xYd7KSDc,41
|
29
34
|
universal_mcp/utils/api_generator.py,sha256=-wRBpLVfJQXy1R-8FpDNs6b8_eeekVDuPc_uwjSGgiY,8883
|
30
35
|
universal_mcp/utils/bridge.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
31
|
-
universal_mcp/utils/docgen.py,sha256=
|
32
|
-
universal_mcp/utils/installation.py,sha256=
|
33
|
-
universal_mcp/utils/openapi.py,sha256=
|
34
|
-
universal_mcp-0.1.
|
35
|
-
universal_mcp-0.1.
|
36
|
-
universal_mcp-0.1.
|
37
|
-
universal_mcp-0.1.
|
36
|
+
universal_mcp/utils/docgen.py,sha256=yK6Ijo8G-wHPU3E1AnFpnXS9vXt2j9FM77w0etTaNOA,12639
|
37
|
+
universal_mcp/utils/installation.py,sha256=uSL_H76fG_7yN4QNxkfp1mEF_00iAPyiXqtdWEMVJe8,3747
|
38
|
+
universal_mcp/utils/openapi.py,sha256=ud_ZB7_60BcS1Vao7ESKDqo0gry9JN5wzy-CFssrjm8,13140
|
39
|
+
universal_mcp-0.1.3rc1.dist-info/METADATA,sha256=AO_09p7mP6jb2TuHbyvAWlrPFSxnbCl1tBcLOeNM_Us,10801
|
40
|
+
universal_mcp-0.1.3rc1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
41
|
+
universal_mcp-0.1.3rc1.dist-info/entry_points.txt,sha256=QlBrVKmA2jIM0q-C-3TQMNJTTWOsOFQvgedBq2rZTS8,56
|
42
|
+
universal_mcp-0.1.3rc1.dist-info/RECORD,,
|