universal-mcp 0.1.8rc3__py3-none-any.whl → 0.1.9__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/__init__.py +7 -2
- 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 +191 -87
- universal_mcp/applications/cal_com_v2/README.md +175 -0
- universal_mcp/applications/cal_com_v2/__init__.py +0 -0
- universal_mcp/applications/cal_com_v2/app.py +5390 -0
- universal_mcp/applications/calendly/app.py +0 -12
- universal_mcp/applications/clickup/README.md +160 -0
- universal_mcp/applications/clickup/__init__.py +0 -0
- universal_mcp/applications/clickup/app.py +5009 -0
- universal_mcp/applications/coda/app.py +0 -33
- universal_mcp/applications/e2b/app.py +2 -28
- universal_mcp/applications/falai/README.md +42 -0
- universal_mcp/applications/falai/__init__.py +0 -0
- universal_mcp/applications/falai/app.py +332 -0
- 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 +2 -32
- universal_mcp/applications/gong/README.md +88 -0
- universal_mcp/applications/gong/__init__.py +0 -0
- universal_mcp/applications/gong/app.py +2297 -0
- universal_mcp/applications/google_calendar/app.py +0 -11
- universal_mcp/applications/google_docs/app.py +0 -18
- universal_mcp/applications/google_drive/app.py +0 -17
- universal_mcp/applications/google_mail/app.py +0 -16
- universal_mcp/applications/google_sheet/app.py +0 -18
- universal_mcp/applications/hashnode/app.py +81 -0
- universal_mcp/applications/hashnode/prompt.md +23 -0
- universal_mcp/applications/heygen/README.md +69 -0
- universal_mcp/applications/heygen/__init__.py +0 -0
- universal_mcp/applications/heygen/app.py +956 -0
- universal_mcp/applications/mailchimp/README.md +306 -0
- universal_mcp/applications/mailchimp/__init__.py +0 -0
- universal_mcp/applications/mailchimp/app.py +10937 -0
- universal_mcp/applications/markitdown/app.py +2 -2
- universal_mcp/applications/perplexity/app.py +0 -35
- universal_mcp/applications/replicate/README.md +65 -0
- universal_mcp/applications/replicate/__init__.py +0 -0
- universal_mcp/applications/replicate/app.py +980 -0
- universal_mcp/applications/resend/app.py +0 -18
- universal_mcp/applications/retell_ai/README.md +46 -0
- universal_mcp/applications/retell_ai/__init__.py +0 -0
- universal_mcp/applications/retell_ai/app.py +333 -0
- universal_mcp/applications/rocketlane/README.md +42 -0
- universal_mcp/applications/rocketlane/__init__.py +0 -0
- universal_mcp/applications/rocketlane/app.py +194 -0
- universal_mcp/applications/serpapi/app.py +2 -28
- universal_mcp/applications/spotify/README.md +116 -0
- universal_mcp/applications/spotify/__init__.py +0 -0
- universal_mcp/applications/spotify/app.py +2526 -0
- universal_mcp/applications/supabase/README.md +112 -0
- universal_mcp/applications/supabase/__init__.py +0 -0
- universal_mcp/applications/supabase/app.py +2970 -0
- universal_mcp/applications/tavily/app.py +0 -20
- universal_mcp/applications/wrike/app.py +0 -12
- universal_mcp/applications/youtube/app.py +0 -18
- universal_mcp/integrations/agentr.py +27 -4
- universal_mcp/integrations/integration.py +14 -6
- universal_mcp/servers/server.py +53 -6
- universal_mcp/stores/store.py +6 -0
- universal_mcp/tools/tools.py +2 -2
- universal_mcp/utils/docstring_parser.py +192 -94
- universal_mcp/utils/installation.py +199 -8
- {universal_mcp-0.1.8rc3.dist-info → universal_mcp-0.1.9.dist-info}/METADATA +6 -1
- universal_mcp-0.1.9.dist-info/RECORD +116 -0
- universal_mcp-0.1.8rc3.dist-info/RECORD +0 -75
- {universal_mcp-0.1.8rc3.dist-info → universal_mcp-0.1.9.dist-info}/WHEEL +0 -0
- {universal_mcp-0.1.8rc3.dist-info → universal_mcp-0.1.9.dist-info}/entry_points.txt +0 -0
@@ -4,16 +4,23 @@ from typing import Any
|
|
4
4
|
|
5
5
|
def parse_docstring(docstring: str | None) -> dict[str, Any]:
|
6
6
|
"""
|
7
|
-
Parses a
|
7
|
+
Parses a Python docstring into structured components: summary, arguments,
|
8
|
+
return value, raised exceptions, and custom tags.
|
9
|
+
|
10
|
+
Supports multi-line descriptions for each section. Recognizes common section
|
11
|
+
headers like 'Args:', 'Returns:', 'Raises:', 'Tags:', etc. Also attempts
|
12
|
+
to parse key-value pairs within 'Args:' and 'Raises:' sections.
|
8
13
|
|
9
14
|
Args:
|
10
|
-
docstring: The docstring to parse.
|
15
|
+
docstring: The docstring string to parse, or None.
|
11
16
|
|
12
17
|
Returns:
|
13
|
-
A dictionary
|
14
|
-
'
|
15
|
-
'
|
16
|
-
'
|
18
|
+
A dictionary containing the parsed components:
|
19
|
+
- 'summary': The first paragraph of the docstring.
|
20
|
+
- 'args': A dictionary mapping argument names to their descriptions.
|
21
|
+
- 'returns': The description of the return value.
|
22
|
+
- 'raises': A dictionary mapping exception types to their descriptions.
|
23
|
+
- 'tags': A list of strings found in the 'Tags:' section.
|
17
24
|
"""
|
18
25
|
if not docstring:
|
19
26
|
return {"summary": "", "args": {}, "returns": "", "raises": {}, "tags": []}
|
@@ -22,123 +29,213 @@ def parse_docstring(docstring: str | None) -> dict[str, Any]:
|
|
22
29
|
if not lines:
|
23
30
|
return {"summary": "", "args": {}, "returns": "", "raises": {}, "tags": []}
|
24
31
|
|
25
|
-
summary =
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
32
|
+
summary: str = ""
|
33
|
+
summary_lines: list[str] = []
|
34
|
+
args: dict[str, str] = {}
|
35
|
+
returns: str = ""
|
36
|
+
raises: dict[str, str] = {}
|
37
|
+
tags: list[str] = []
|
38
|
+
|
39
|
+
current_section: str | None = None
|
40
|
+
current_key: str | None = None
|
41
|
+
current_desc_lines: list[str] = []
|
42
|
+
|
43
|
+
# Pattern to capture item key and the start of its description
|
44
|
+
# Matches "key:" or "key (type):" followed by description
|
33
45
|
key_pattern = re.compile(r"^\s*([\w\.]+)\s*(?:\(.*\))?:\s*(.*)")
|
34
46
|
|
35
47
|
def finalize_current_item():
|
36
|
-
"""
|
37
|
-
nonlocal returns, tags
|
48
|
+
"""Processes the collected current_desc_lines and assigns them."""
|
49
|
+
nonlocal returns, tags, args, raises
|
38
50
|
desc = " ".join(current_desc_lines).strip()
|
51
|
+
|
39
52
|
if current_section == "args" and current_key:
|
40
|
-
|
53
|
+
if desc:
|
54
|
+
args[current_key] = desc
|
41
55
|
elif current_section == "raises" and current_key:
|
42
|
-
|
56
|
+
if desc:
|
57
|
+
raises[current_key] = desc
|
43
58
|
elif current_section == "returns":
|
44
59
|
returns = desc
|
45
|
-
|
46
|
-
|
47
|
-
tags
|
48
|
-
|
49
|
-
|
50
|
-
for _, line in enumerate(lines[1:]):
|
51
|
-
stripped_line = line.strip()
|
52
|
-
original_indentation = len(line) - len(line.lstrip(" "))
|
60
|
+
elif current_section == "tags":
|
61
|
+
# Tags section content is treated as a comma-separated list
|
62
|
+
tags.clear() # Clear existing tags in case of multiple tag sections (unlikely but safe)
|
63
|
+
tags.extend([tag.strip() for tag in desc.split(",") if tag.strip()])
|
64
|
+
# 'other' sections are ignored in the final output
|
53
65
|
|
54
|
-
|
55
|
-
|
56
|
-
|
66
|
+
def check_for_section_header(line: str) -> tuple[bool, str | None, str]:
|
67
|
+
"""Checks if a line is a recognized section header."""
|
68
|
+
stripped_lower = line.strip().lower()
|
69
|
+
section_type: str | None = None
|
57
70
|
header_content = ""
|
58
71
|
|
59
|
-
if
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
elif
|
66
|
-
|
67
|
-
|
68
|
-
elif
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
72
|
+
if stripped_lower in ("args:", "arguments:", "parameters:"):
|
73
|
+
section_type = "args"
|
74
|
+
elif stripped_lower in ("returns:", "yields:"):
|
75
|
+
section_type = "returns"
|
76
|
+
elif stripped_lower in ("raises:", "errors:", "exceptions:"):
|
77
|
+
section_type = "raises"
|
78
|
+
elif stripped_lower in ("tags:",):
|
79
|
+
section_type = "tags"
|
80
|
+
# Allow "Raises Description:" or "Tags content:"
|
81
|
+
elif stripped_lower.startswith(("raises ", "errors ", "exceptions ")):
|
82
|
+
section_type = "raises"
|
83
|
+
# Capture content after header word and potential colon/space
|
84
|
+
parts = re.split(
|
85
|
+
r"[:\s]+", line.strip(), maxsplit=1
|
86
|
+
) # B034: Use keyword maxsplit
|
87
|
+
if len(parts) > 1:
|
88
|
+
header_content = parts[1].strip()
|
89
|
+
elif stripped_lower.startswith(("tags",)):
|
90
|
+
section_type = "tags"
|
91
|
+
# Capture content after header word and potential colon/space
|
92
|
+
parts = re.split(
|
93
|
+
r"[:\s]+", line.strip(), maxsplit=1
|
94
|
+
) # B034: Use keyword maxsplit
|
95
|
+
if len(parts) > 1:
|
96
|
+
header_content = parts[1].strip()
|
97
|
+
|
98
|
+
# Identify other known sections, but don't store their content
|
99
|
+
elif stripped_lower.endswith(":") and stripped_lower[:-1] in (
|
76
100
|
"attributes",
|
77
101
|
"see also",
|
78
102
|
"example",
|
79
103
|
"examples",
|
80
104
|
"notes",
|
105
|
+
"todo",
|
106
|
+
"fixme",
|
107
|
+
"warning",
|
108
|
+
"warnings",
|
81
109
|
):
|
82
|
-
|
83
|
-
is_new_section_header = True
|
110
|
+
section_type = "other"
|
84
111
|
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
112
|
+
return section_type is not None, section_type, header_content
|
113
|
+
|
114
|
+
in_summary = True
|
115
|
+
|
116
|
+
for line in lines:
|
117
|
+
stripped_line = line.strip()
|
118
|
+
original_indentation = len(line) - len(line.lstrip(" "))
|
119
|
+
|
120
|
+
is_new_section_header, new_section_type_this_line, header_content_this_line = (
|
121
|
+
check_for_section_header(line)
|
122
|
+
)
|
123
|
+
|
124
|
+
should_finalize_previous = False
|
125
|
+
|
126
|
+
# --- Summary Handling ---
|
127
|
+
if in_summary:
|
128
|
+
if not stripped_line or is_new_section_header:
|
129
|
+
# Empty line or section header marks the end of the summary
|
130
|
+
in_summary = False
|
131
|
+
summary = " ".join(summary_lines).strip()
|
132
|
+
summary_lines = [] # Clear summary_lines after finalizing summary
|
133
|
+
|
134
|
+
if not stripped_line:
|
135
|
+
# If the line was just empty, continue to the next line
|
136
|
+
# The new_section_header check will happen on the next iteration if it exists
|
137
|
+
continue
|
138
|
+
# If it was a header, fall through to section handling below
|
139
|
+
|
140
|
+
else:
|
141
|
+
# Still in summary, append line
|
142
|
+
summary_lines.append(stripped_line)
|
143
|
+
continue # Process next line
|
144
|
+
|
145
|
+
# --- Section and Item Handling ---
|
146
|
+
|
147
|
+
# Decide if the previous item/section block should be finalized BEFORE processing the current line
|
148
|
+
# Finalize if:
|
149
|
+
# 1. A new section header is encountered.
|
150
|
+
# 2. An empty line is encountered AFTER we've started collecting content for an item or section.
|
151
|
+
# 3. In 'args' or 'raises', we encounter a line that looks like a new key: value pair, or a non-indented line.
|
152
|
+
# 4. In 'returns', 'tags', or 'other', we encounter a non-indented line after collecting content.
|
153
|
+
if (
|
154
|
+
is_new_section_header
|
155
|
+
or (not stripped_line and (current_desc_lines or current_key is not None))
|
156
|
+
or (
|
157
|
+
current_section in ["args", "raises"]
|
158
|
+
and current_key is not None
|
159
|
+
and (
|
160
|
+
key_pattern.match(line)
|
161
|
+
or (original_indentation == 0 and stripped_line)
|
162
|
+
)
|
163
|
+
)
|
164
|
+
or (
|
165
|
+
current_section in ["returns", "tags", "other"]
|
166
|
+
and current_desc_lines
|
167
|
+
and original_indentation == 0
|
168
|
+
and stripped_line
|
169
|
+
)
|
100
170
|
):
|
101
|
-
|
171
|
+
should_finalize_previous = True
|
172
|
+
elif current_section in ["args", "raises"] and current_key is not None:
|
173
|
+
# Inside args/raises, processing an item (current_key is set)
|
174
|
+
pass # Logic moved to the combined if statement
|
175
|
+
elif current_section in ["returns", "tags", "other"] and current_desc_lines:
|
176
|
+
# Inside returns/tags/other, collecting description lines
|
177
|
+
pass # Logic moved to the combined if statement
|
102
178
|
|
103
|
-
|
179
|
+
# If finalizing the previous item/section
|
180
|
+
if should_finalize_previous:
|
104
181
|
finalize_current_item()
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
182
|
+
# Reset state after finalizing the previous item/section block
|
183
|
+
# If it was a new section header, reset everything
|
184
|
+
# If it was an end-of-item/block signal within a section, reset key and description lines
|
185
|
+
# (The condition for resetting key here is complex but matches the original logic)
|
186
|
+
if is_new_section_header or (
|
187
|
+
current_section in ["args", "raises"]
|
188
|
+
and current_key is not None
|
189
|
+
and not key_pattern.match(line)
|
190
|
+
and (not stripped_line or original_indentation == 0)
|
191
|
+
):
|
192
|
+
current_key = None
|
193
|
+
current_desc_lines = [] # Always clear description lines
|
194
|
+
|
195
|
+
# --- Process the current line ---
|
117
196
|
|
197
|
+
# If the current line is a section header
|
198
|
+
if is_new_section_header:
|
199
|
+
current_section = new_section_type_this_line
|
200
|
+
if header_content_this_line:
|
201
|
+
# Add content immediately following the header on the same line
|
202
|
+
current_desc_lines.append(header_content_this_line)
|
203
|
+
continue # Move to the next line, header is processed
|
204
|
+
|
205
|
+
# If the line is empty, and not a section header (handled above), skip it
|
118
206
|
if not stripped_line:
|
119
207
|
continue
|
120
208
|
|
209
|
+
# If we are inside a section, process the line's content
|
121
210
|
if current_section == "args" or current_section == "raises":
|
122
211
|
match = key_pattern.match(line)
|
123
212
|
if match:
|
213
|
+
# Found a new key: value item within args/raises
|
124
214
|
current_key = match.group(1)
|
125
215
|
current_desc_lines = [match.group(2).strip()] # Start new description
|
126
|
-
elif
|
127
|
-
|
128
|
-
): # Check for indentation for continuation
|
216
|
+
elif current_key is not None:
|
217
|
+
# Not a new key, but processing an existing item - append to description
|
129
218
|
current_desc_lines.append(stripped_line)
|
219
|
+
# Lines that don't match key_pattern and occur when current_key is None
|
220
|
+
# within args/raises are effectively ignored by this block, which seems
|
221
|
+
# consistent with needing a key: description format.
|
130
222
|
|
131
|
-
elif current_section
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
elif current_section == "tags":
|
136
|
-
if (
|
137
|
-
original_indentation > 0 or not current_desc_lines
|
138
|
-
): # Indented or first line
|
139
|
-
current_desc_lines.append(stripped_line)
|
223
|
+
elif current_section in ["returns", "tags", "other"]:
|
224
|
+
# In these sections, all non-empty, non-header lines are description lines
|
225
|
+
current_desc_lines.append(stripped_line)
|
140
226
|
|
227
|
+
# --- Finalization after loop ---
|
228
|
+
# Finalize any pending item/section block that was being collected
|
141
229
|
finalize_current_item()
|
230
|
+
|
231
|
+
# If the docstring only had a summary (no empty line or section header)
|
232
|
+
# ensure the summary is captured. This check is technically redundant
|
233
|
+
# because summary is finalized upon hitting the first empty line or header,
|
234
|
+
# or falls through to the final finalize call if neither occurs.
|
235
|
+
# Keeping it for clarity, though the logic flow should cover it.
|
236
|
+
if in_summary:
|
237
|
+
summary = " ".join(summary_lines).strip()
|
238
|
+
|
142
239
|
return {
|
143
240
|
"summary": summary,
|
144
241
|
"args": args,
|
@@ -149,7 +246,8 @@ def parse_docstring(docstring: str | None) -> dict[str, Any]:
|
|
149
246
|
|
150
247
|
|
151
248
|
docstring_example = """
|
152
|
-
Starts a crawl job for a given URL using Firecrawl.
|
249
|
+
Starts a crawl job for a given URL using Firecrawl.
|
250
|
+
Returns the job ID immediately.
|
153
251
|
|
154
252
|
Args:
|
155
253
|
url: The starting URL for the crawl.
|
@@ -163,17 +261,17 @@ docstring_example = """
|
|
163
261
|
or a string containing an error message on failure. This description
|
164
262
|
can also span multiple lines.
|
165
263
|
|
166
|
-
|
264
|
+
Raises:
|
167
265
|
ValueError: If the URL is invalid.
|
168
|
-
|
266
|
+
ConnectionError: If connection fails.
|
169
267
|
|
170
268
|
Tags:
|
171
269
|
crawl, async_job, start, api, long_tag_example , another
|
172
270
|
, final_tag
|
173
|
-
"""
|
271
|
+
"""
|
174
272
|
|
175
273
|
if __name__ == "__main__":
|
176
|
-
parsed = parse_docstring(docstring_example)
|
177
274
|
import json
|
178
275
|
|
276
|
+
parsed = parse_docstring(docstring_example)
|
179
277
|
print(json.dumps(parsed, indent=4))
|
@@ -29,7 +29,7 @@ def create_file_if_not_exists(path: Path) -> None:
|
|
29
29
|
|
30
30
|
def get_supported_apps() -> list[str]:
|
31
31
|
"""Get list of supported apps"""
|
32
|
-
return ["claude", "cursor", "windsurf"]
|
32
|
+
return ["claude", "cursor", "cline", "continue", "goose", "windsurf", "zed"]
|
33
33
|
|
34
34
|
|
35
35
|
def install_claude(api_key: str) -> None:
|
@@ -99,21 +99,212 @@ def install_cursor(api_key: str) -> None:
|
|
99
99
|
print("[green]✓[/green] Cursor configuration installed successfully")
|
100
100
|
|
101
101
|
|
102
|
-
def
|
102
|
+
def install_cline(api_key: str) -> None:
|
103
|
+
"""Install Cline"""
|
104
|
+
print("[bold blue]Installing Cline configuration...[/bold blue]")
|
105
|
+
# Set up Cline config path
|
106
|
+
config_path = Path.home() / ".config/cline/mcp.json"
|
107
|
+
|
108
|
+
# Create config directory if it doesn't exist
|
109
|
+
create_file_if_not_exists(config_path)
|
110
|
+
|
111
|
+
try:
|
112
|
+
config = json.loads(config_path.read_text())
|
113
|
+
except json.JSONDecodeError:
|
114
|
+
print(
|
115
|
+
"[yellow]Config file was empty or invalid, creating new configuration[/yellow]"
|
116
|
+
)
|
117
|
+
config = {}
|
118
|
+
|
119
|
+
if "mcpServers" not in config:
|
120
|
+
config["mcpServers"] = {}
|
121
|
+
config["mcpServers"]["universal_mcp"] = {
|
122
|
+
"command": get_uvx_path(),
|
123
|
+
"args": ["universal_mcp[all]@latest", "run"],
|
124
|
+
"env": {"AGENTR_API_KEY": api_key},
|
125
|
+
}
|
126
|
+
|
127
|
+
with open(config_path, "w") as f:
|
128
|
+
json.dump(config, f, indent=4)
|
129
|
+
print("[green]✓[/green] Cline configuration installed successfully")
|
130
|
+
|
131
|
+
|
132
|
+
def install_continue(api_key: str) -> None:
|
133
|
+
"""Install Continue"""
|
134
|
+
print("[bold blue]Installing Continue configuration...[/bold blue]")
|
135
|
+
|
136
|
+
# Determine platform-specific config path
|
137
|
+
if sys.platform == "darwin": # macOS
|
138
|
+
config_path = Path.home() / "Library/Application Support/Continue/mcp.json"
|
139
|
+
elif sys.platform == "win32": # Windows
|
140
|
+
config_path = Path.home() / "AppData/Roaming/Continue/mcp.json"
|
141
|
+
else: # Linux and others
|
142
|
+
config_path = Path.home() / ".config/continue/mcp.json"
|
143
|
+
|
144
|
+
# Create config directory if it doesn't exist
|
145
|
+
create_file_if_not_exists(config_path)
|
146
|
+
|
147
|
+
try:
|
148
|
+
config = json.loads(config_path.read_text())
|
149
|
+
except json.JSONDecodeError:
|
150
|
+
print(
|
151
|
+
"[yellow]Config file was empty or invalid, creating new configuration[/yellow]"
|
152
|
+
)
|
153
|
+
config = {}
|
154
|
+
|
155
|
+
if "mcpServers" not in config:
|
156
|
+
config["mcpServers"] = {}
|
157
|
+
config["mcpServers"]["universal_mcp"] = {
|
158
|
+
"command": get_uvx_path(),
|
159
|
+
"args": ["universal_mcp[all]@latest", "run"],
|
160
|
+
"env": {"AGENTR_API_KEY": api_key},
|
161
|
+
}
|
162
|
+
|
163
|
+
with open(config_path, "w") as f:
|
164
|
+
json.dump(config, f, indent=4)
|
165
|
+
print("[green]✓[/green] Continue configuration installed successfully")
|
166
|
+
|
167
|
+
|
168
|
+
def install_goose(api_key: str) -> None:
|
169
|
+
"""Install Goose"""
|
170
|
+
print("[bold blue]Installing Goose configuration...[/bold blue]")
|
171
|
+
|
172
|
+
# Determine platform-specific config path
|
173
|
+
if sys.platform == "darwin": # macOS
|
174
|
+
config_path = Path.home() / "Library/Application Support/Goose/mcp-config.json"
|
175
|
+
elif sys.platform == "win32": # Windows
|
176
|
+
config_path = Path.home() / "AppData/Roaming/Goose/mcp-config.json"
|
177
|
+
else: # Linux and others
|
178
|
+
config_path = Path.home() / ".config/goose/mcp-config.json"
|
179
|
+
|
180
|
+
# Create config directory if it doesn't exist
|
181
|
+
create_file_if_not_exists(config_path)
|
182
|
+
|
183
|
+
try:
|
184
|
+
config = json.loads(config_path.read_text())
|
185
|
+
except json.JSONDecodeError:
|
186
|
+
print(
|
187
|
+
"[yellow]Config file was empty or invalid, creating new configuration[/yellow]"
|
188
|
+
)
|
189
|
+
config = {}
|
190
|
+
|
191
|
+
if "mcpServers" not in config:
|
192
|
+
config["mcpServers"] = {}
|
193
|
+
config["mcpServers"]["universal_mcp"] = {
|
194
|
+
"command": get_uvx_path(),
|
195
|
+
"args": ["universal_mcp[all]@latest", "run"],
|
196
|
+
"env": {"AGENTR_API_KEY": api_key},
|
197
|
+
}
|
198
|
+
|
199
|
+
with open(config_path, "w") as f:
|
200
|
+
json.dump(config, f, indent=4)
|
201
|
+
print("[green]✓[/green] Goose configuration installed successfully")
|
202
|
+
|
203
|
+
|
204
|
+
def install_windsurf(api_key: str) -> None:
|
103
205
|
"""Install Windsurf"""
|
104
|
-
print("[
|
105
|
-
|
206
|
+
print("[bold blue]Installing Windsurf configuration...[/bold blue]")
|
207
|
+
|
208
|
+
# Determine platform-specific config path
|
209
|
+
if sys.platform == "darwin": # macOS
|
210
|
+
config_path = Path.home() / "Library/Application Support/Windsurf/mcp.json"
|
211
|
+
elif sys.platform == "win32": # Windows
|
212
|
+
config_path = Path.home() / "AppData/Roaming/Windsurf/mcp.json"
|
213
|
+
else: # Linux and others
|
214
|
+
config_path = Path.home() / ".config/windsurf/mcp.json"
|
215
|
+
|
216
|
+
# Create config directory if it doesn't exist
|
217
|
+
create_file_if_not_exists(config_path)
|
218
|
+
|
219
|
+
try:
|
220
|
+
config = json.loads(config_path.read_text())
|
221
|
+
except json.JSONDecodeError:
|
222
|
+
print(
|
223
|
+
"[yellow]Config file was empty or invalid, creating new configuration[/yellow]"
|
224
|
+
)
|
225
|
+
config = {}
|
226
|
+
|
227
|
+
if "mcpServers" not in config:
|
228
|
+
config["mcpServers"] = {}
|
229
|
+
config["mcpServers"]["universal_mcp"] = {
|
230
|
+
"command": get_uvx_path(),
|
231
|
+
"args": ["universal_mcp[all]@latest", "run"],
|
232
|
+
"env": {"AGENTR_API_KEY": api_key},
|
233
|
+
}
|
234
|
+
|
235
|
+
with open(config_path, "w") as f:
|
236
|
+
json.dump(config, f, indent=4)
|
237
|
+
print("[green]✓[/green] Windsurf configuration installed successfully")
|
238
|
+
|
239
|
+
|
240
|
+
def install_zed(api_key: str) -> None:
|
241
|
+
"""Install Zed"""
|
242
|
+
print("[bold blue]Installing Zed configuration...[/bold blue]")
|
243
|
+
|
244
|
+
# Set up Zed config path
|
245
|
+
config_dir = Path.home() / ".config/zed"
|
246
|
+
config_path = config_dir / "mcp_servers.json"
|
247
|
+
|
248
|
+
# Create config directory if it doesn't exist
|
249
|
+
create_file_if_not_exists(config_path)
|
250
|
+
|
251
|
+
try:
|
252
|
+
config = json.loads(config_path.read_text())
|
253
|
+
except json.JSONDecodeError:
|
254
|
+
print(
|
255
|
+
"[yellow]Config file was empty or invalid, creating new configuration[/yellow]"
|
256
|
+
)
|
257
|
+
config = {}
|
258
|
+
|
259
|
+
if not isinstance(config, list):
|
260
|
+
config = []
|
261
|
+
|
262
|
+
# Check if universal_mcp is already in the config
|
263
|
+
existing_config = False
|
264
|
+
for server in config:
|
265
|
+
if server.get("name") == "universal_mcp":
|
266
|
+
existing_config = True
|
267
|
+
server.update(
|
268
|
+
{
|
269
|
+
"command": get_uvx_path(),
|
270
|
+
"args": ["universal_mcp[all]@latest", "run"],
|
271
|
+
"env": {"AGENTR_API_KEY": api_key},
|
272
|
+
}
|
273
|
+
)
|
274
|
+
break
|
275
|
+
|
276
|
+
if not existing_config:
|
277
|
+
config.append(
|
278
|
+
{
|
279
|
+
"name": "universal_mcp",
|
280
|
+
"command": get_uvx_path(),
|
281
|
+
"args": ["universal_mcp[all]@latest", "run"],
|
282
|
+
"env": {"AGENTR_API_KEY": api_key},
|
283
|
+
}
|
284
|
+
)
|
285
|
+
|
286
|
+
with open(config_path, "w") as f:
|
287
|
+
json.dump(config, f, indent=4)
|
288
|
+
print("[green]✓[/green] Zed configuration installed successfully")
|
106
289
|
|
107
290
|
|
108
|
-
def install_app(app_name: str) -> None:
|
291
|
+
def install_app(app_name: str, api_key: str) -> None:
|
109
292
|
"""Install an app"""
|
110
293
|
print(f"[bold]Installing {app_name}...[/bold]")
|
111
294
|
if app_name == "claude":
|
112
|
-
install_claude()
|
295
|
+
install_claude(api_key)
|
113
296
|
elif app_name == "cursor":
|
114
|
-
install_cursor()
|
297
|
+
install_cursor(api_key)
|
298
|
+
elif app_name == "cline":
|
299
|
+
install_cline(api_key)
|
300
|
+
elif app_name == "continue":
|
301
|
+
install_continue(api_key)
|
302
|
+
elif app_name == "goose":
|
303
|
+
install_goose(api_key)
|
115
304
|
elif app_name == "windsurf":
|
116
|
-
install_windsurf()
|
305
|
+
install_windsurf(api_key)
|
306
|
+
elif app_name == "zed":
|
307
|
+
install_zed(api_key)
|
117
308
|
else:
|
118
309
|
print(f"[red]Error: App '{app_name}' not supported[/red]")
|
119
310
|
raise ValueError(f"App '{app_name}' not supported")
|
@@ -1,10 +1,13 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: universal-mcp
|
3
|
-
Version: 0.1.
|
3
|
+
Version: 0.1.9
|
4
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
5
|
Author-email: Manoj Bajaj <manojbajaj95@gmail.com>
|
6
6
|
Requires-Python: >=3.11
|
7
|
+
Requires-Dist: gql[all]>=3.5.2
|
7
8
|
Requires-Dist: keyring>=25.6.0
|
9
|
+
Requires-Dist: langchain-cerebras>=0.5.0
|
10
|
+
Requires-Dist: langchain-google-genai>=2.1.3
|
8
11
|
Requires-Dist: litellm>=1.30.7
|
9
12
|
Requires-Dist: loguru>=0.7.3
|
10
13
|
Requires-Dist: mcp>=1.6.0
|
@@ -22,6 +25,8 @@ Requires-Dist: pytest>=8.3.5; extra == 'dev'
|
|
22
25
|
Requires-Dist: ruff>=0.11.4; extra == 'dev'
|
23
26
|
Provides-Extra: e2b
|
24
27
|
Requires-Dist: e2b-code-interpreter>=1.2.0; extra == 'e2b'
|
28
|
+
Provides-Extra: fal-ai
|
29
|
+
Requires-Dist: fal-client>=0.5.9; extra == 'fal-ai'
|
25
30
|
Provides-Extra: firecrawl
|
26
31
|
Requires-Dist: firecrawl-py>=1.15.0; extra == 'firecrawl'
|
27
32
|
Provides-Extra: markitdown
|