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.
@@ -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
- app = self._load_app(app_config)
105
- if app:
106
- tools = app.list_tools()
110
+ try:
111
+ app = self._load_app(app_config)
112
+ if app:
113
+ tools = app.list_tools()
107
114
  for tool in tools:
108
- full_tool_name = app.name + "_" + tool.__name__
115
+ tool_name = tool.__name__
116
+ name = app.name + "_" + tool_name
109
117
  description = tool.__doc__
110
- should_add_tool = False
111
- if (
112
- app_config.actions is None
113
- or full_tool_name in app_config.actions
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 app in apps:
160
- app = self._load_app(app)
161
- if app:
162
- tools = app.list_tools()
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
- name = app.name + "_" + tool.__name__
167
+ tool_name = tool.__name__
168
+ name = app.name + "_" + tool_name
165
169
  description = tool.__doc__
166
- self.add_tool(tool, name=name, description=description)
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}")
@@ -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 = "google/gemini-flash"
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 = "google/gemini-flash") -> int:
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")
@@ -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 (List[Any], Dict[str, Any], or Any)
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 "List[Any]"
65
+ return "list[Any]"
66
66
  elif schema.get("type") == "object" or "$ref" in schema:
67
- return "Dict[str, Any]"
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
- # Add tool name prefix if provided
197
- if tool_name:
198
- func_name = f"{tool_name}_{func_name}"
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
- parameters = operation.get("parameters", [])
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
- if param.get("required", False):
210
- required_args.append(param["name"])
211
- else:
212
- optional_args.append(f"{param['name']}=None")
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 param in parameters:
234
- if param.get("required", False):
235
- body_lines.append(f" if {param['name']} is None:")
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 '{param['name']}'\")"
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
- # Path parameters
248
- path_params = [p for p in parameters if p["in"] == "path"]
249
- path_params_dict = ", ".join([f"'{p['name']}': {p['name']}" for p in path_params])
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
- " response = self._put(url, data={}, params=query_params)"
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
- body_lines.append(
301
- f" response = self._{method_lower}(url, data={{}}, params=query_params)"
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=rLdE1CRiouPIL2nBYv9EInOk_1uJfUBHmakWHBZ0roc,5555
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=AOEQkaOpbkXeMtNdsPKmVQFd-4YHgjpjZy4j0ZvY9co,16831
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=LV8cvkhmacsal-mJmKL9DH5BMypL9MGHIiCkhal6Jtg,1019
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=zMrSoubzdmJLYr-SKSDMBLWOwIG4-g0l5UEu3xmzsjY,5622
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=tSbHfFVqsksMIrMcKDPiJdBZYhrpsx3QXyEuvBnUAU0,5893
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=UUjiRcIeb96xbogF96Ujzw3Hdd5ExckOao5rzIpRsBQ,12651
32
- universal_mcp/utils/installation.py,sha256=nyuQDl8S6KftjukCOKE4vtiqSzpVO7M5U-W00ivp444,2939
33
- universal_mcp/utils/openapi.py,sha256=A719DaYc7AgN-n_uW868MBmpG_oPvuvMOgUCauInLeY,12629
34
- universal_mcp-0.1.2rc1.dist-info/METADATA,sha256=ImRyeQyPY4NxFMJTMCazXYeIoJGXQ_ra4o8Z7e8l-X0,5871
35
- universal_mcp-0.1.2rc1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
36
- universal_mcp-0.1.2rc1.dist-info/entry_points.txt,sha256=QlBrVKmA2jIM0q-C-3TQMNJTTWOsOFQvgedBq2rZTS8,56
37
- universal_mcp-0.1.2rc1.dist-info/RECORD,,
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,,