universal-mcp 0.1.7rc2__py3-none-any.whl → 0.1.8__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/app.py +2291 -0
- universal_mcp/applications/application.py +95 -5
- universal_mcp/applications/calendly/README.md +78 -0
- universal_mcp/applications/calendly/__init__.py +0 -0
- universal_mcp/applications/calendly/app.py +1195 -0
- 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 +14 -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 +29 -32
- universal_mcp/applications/github/app.py +127 -85
- universal_mcp/applications/google_calendar/app.py +62 -138
- universal_mcp/applications/google_docs/app.py +47 -52
- universal_mcp/applications/google_drive/app.py +119 -113
- universal_mcp/applications/google_mail/app.py +124 -50
- universal_mcp/applications/google_sheet/app.py +89 -91
- universal_mcp/applications/markitdown/app.py +9 -8
- universal_mcp/applications/notion/app.py +254 -134
- universal_mcp/applications/perplexity/app.py +13 -45
- universal_mcp/applications/reddit/app.py +94 -85
- universal_mcp/applications/resend/app.py +12 -23
- universal_mcp/applications/{serp → serpapi}/app.py +14 -33
- universal_mcp/applications/tavily/app.py +11 -28
- universal_mcp/applications/wrike/README.md +71 -0
- universal_mcp/applications/wrike/__init__.py +0 -0
- universal_mcp/applications/wrike/app.py +1372 -0
- universal_mcp/applications/youtube/README.md +82 -0
- universal_mcp/applications/youtube/__init__.py +0 -0
- universal_mcp/applications/youtube/app.py +1428 -0
- universal_mcp/applications/zenquotes/app.py +12 -2
- universal_mcp/exceptions.py +9 -2
- universal_mcp/integrations/__init__.py +24 -1
- universal_mcp/integrations/agentr.py +27 -4
- universal_mcp/integrations/integration.py +143 -30
- universal_mcp/logger.py +3 -56
- universal_mcp/servers/__init__.py +6 -14
- universal_mcp/servers/server.py +201 -146
- universal_mcp/stores/__init__.py +7 -2
- universal_mcp/stores/store.py +103 -40
- universal_mcp/tools/__init__.py +3 -0
- universal_mcp/tools/adapters.py +43 -0
- universal_mcp/tools/func_metadata.py +213 -0
- universal_mcp/tools/tools.py +342 -0
- universal_mcp/utils/docgen.py +325 -119
- universal_mcp/utils/docstring_parser.py +179 -0
- universal_mcp/utils/dump_app_tools.py +33 -23
- universal_mcp/utils/installation.py +199 -8
- universal_mcp/utils/openapi.py +229 -46
- {universal_mcp-0.1.7rc2.dist-info → universal_mcp-0.1.8.dist-info}/METADATA +9 -5
- universal_mcp-0.1.8.dist-info/RECORD +81 -0
- universal_mcp-0.1.7rc2.dist-info/RECORD +0 -58
- /universal_mcp/{utils/bridge.py → applications/ahrefs/__init__.py} +0 -0
- /universal_mcp/applications/{serp → serpapi}/README.md +0 -0
- {universal_mcp-0.1.7rc2.dist-info → universal_mcp-0.1.8.dist-info}/WHEEL +0 -0
- {universal_mcp-0.1.7rc2.dist-info → universal_mcp-0.1.8.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,179 @@
|
|
1
|
+
import re
|
2
|
+
from typing import Any
|
3
|
+
|
4
|
+
|
5
|
+
def parse_docstring(docstring: str | None) -> dict[str, Any]:
|
6
|
+
"""
|
7
|
+
Parses a standard Python docstring into summary, args, returns, raises, and tags.
|
8
|
+
|
9
|
+
Args:
|
10
|
+
docstring: The docstring to parse.
|
11
|
+
|
12
|
+
Returns:
|
13
|
+
A dictionary with keys 'summary', 'args', 'returns', 'raises', 'tags'.
|
14
|
+
'args' is a dict mapping arg names to descriptions.
|
15
|
+
'raises' is a dict mapping exception type names to descriptions.
|
16
|
+
'tags' is a list of strings extracted from the 'Tags:' section, comma-separated.
|
17
|
+
"""
|
18
|
+
if not docstring:
|
19
|
+
return {"summary": "", "args": {}, "returns": "", "raises": {}, "tags": []}
|
20
|
+
|
21
|
+
lines = docstring.strip().splitlines()
|
22
|
+
if not lines:
|
23
|
+
return {"summary": "", "args": {}, "returns": "", "raises": {}, "tags": []}
|
24
|
+
|
25
|
+
summary = lines[0].strip()
|
26
|
+
args = {}
|
27
|
+
returns = ""
|
28
|
+
raises = {}
|
29
|
+
tags: list[str] = [] # Final list of parsed tags
|
30
|
+
current_section = None
|
31
|
+
current_key = None
|
32
|
+
current_desc_lines = [] # Accumulator for multi-line descriptions/tag content
|
33
|
+
key_pattern = re.compile(r"^\s*([\w\.]+)\s*(?:\(.*\))?:\s*(.*)")
|
34
|
+
|
35
|
+
def finalize_current_item():
|
36
|
+
"""Helper function to finalize the currently parsed item."""
|
37
|
+
nonlocal returns, tags # Allow modification of outer scope variables
|
38
|
+
desc = " ".join(current_desc_lines).strip()
|
39
|
+
if current_section == "args" and current_key:
|
40
|
+
args[current_key] = desc
|
41
|
+
elif current_section == "raises" and current_key:
|
42
|
+
raises[current_key] = desc
|
43
|
+
elif current_section == "returns":
|
44
|
+
returns = desc
|
45
|
+
# SIM102 applied: Combine nested if
|
46
|
+
elif current_section == "tags" and desc: # Only process if there's content
|
47
|
+
tags = [tag.strip() for tag in desc.split(",") if tag.strip()]
|
48
|
+
|
49
|
+
# B007 applied: Rename unused loop variable i to _
|
50
|
+
for _, line in enumerate(lines[1:]):
|
51
|
+
stripped_line = line.strip()
|
52
|
+
original_indentation = len(line) - len(line.lstrip(" "))
|
53
|
+
|
54
|
+
section_line = stripped_line.lower()
|
55
|
+
is_new_section_header = False
|
56
|
+
new_section_type = None
|
57
|
+
header_content = ""
|
58
|
+
|
59
|
+
if section_line in ("args:", "arguments:", "parameters:"):
|
60
|
+
new_section_type = "args"
|
61
|
+
is_new_section_header = True
|
62
|
+
elif section_line in ("returns:", "yields:"):
|
63
|
+
new_section_type = "returns"
|
64
|
+
is_new_section_header = True
|
65
|
+
elif section_line.startswith(("raises ", "raises:", "errors:", "exceptions:")):
|
66
|
+
new_section_type = "raises"
|
67
|
+
is_new_section_header = True
|
68
|
+
elif section_line.startswith(
|
69
|
+
("tags:", "tags")
|
70
|
+
): # Match "Tags:" or "Tags" potentially followed by content
|
71
|
+
new_section_type = "tags"
|
72
|
+
is_new_section_header = True
|
73
|
+
if ":" in stripped_line:
|
74
|
+
header_content = stripped_line.split(":", 1)[1].strip()
|
75
|
+
elif section_line.endswith(":") and section_line[:-1] in (
|
76
|
+
"attributes",
|
77
|
+
"see also",
|
78
|
+
"example",
|
79
|
+
"examples",
|
80
|
+
"notes",
|
81
|
+
):
|
82
|
+
new_section_type = "other"
|
83
|
+
is_new_section_header = True
|
84
|
+
|
85
|
+
finalize_previous = False
|
86
|
+
if is_new_section_header:
|
87
|
+
finalize_previous = True
|
88
|
+
elif current_section in ["args", "raises"] and current_key:
|
89
|
+
if key_pattern.match(line) or (original_indentation == 0 and stripped_line):
|
90
|
+
finalize_previous = True
|
91
|
+
elif current_section in ["returns", "tags"] and current_desc_lines:
|
92
|
+
if original_indentation == 0 and stripped_line:
|
93
|
+
finalize_previous = True
|
94
|
+
# SIM102 applied: Combine nested if/elif
|
95
|
+
elif (
|
96
|
+
not stripped_line
|
97
|
+
and current_desc_lines
|
98
|
+
and current_section in ["args", "raises", "returns", "tags"]
|
99
|
+
and (current_section not in ["args", "raises"] or current_key)
|
100
|
+
):
|
101
|
+
finalize_previous = True
|
102
|
+
|
103
|
+
if finalize_previous:
|
104
|
+
finalize_current_item()
|
105
|
+
current_key = None
|
106
|
+
current_desc_lines = []
|
107
|
+
if not is_new_section_header or new_section_type == "other":
|
108
|
+
current_section = None
|
109
|
+
|
110
|
+
if is_new_section_header and new_section_type != "other":
|
111
|
+
current_section = new_section_type
|
112
|
+
# If Tags header had content, start accumulating it
|
113
|
+
if new_section_type == "tags" and header_content:
|
114
|
+
current_desc_lines.append(header_content)
|
115
|
+
# Don't process the header line itself further
|
116
|
+
continue
|
117
|
+
|
118
|
+
if not stripped_line:
|
119
|
+
continue
|
120
|
+
|
121
|
+
if current_section == "args" or current_section == "raises":
|
122
|
+
match = key_pattern.match(line)
|
123
|
+
if match:
|
124
|
+
current_key = match.group(1)
|
125
|
+
current_desc_lines = [match.group(2).strip()] # Start new description
|
126
|
+
elif (
|
127
|
+
current_key and original_indentation > 0
|
128
|
+
): # Check for indentation for continuation
|
129
|
+
current_desc_lines.append(stripped_line)
|
130
|
+
|
131
|
+
elif current_section == "returns":
|
132
|
+
if not current_desc_lines or original_indentation > 0:
|
133
|
+
current_desc_lines.append(stripped_line)
|
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)
|
140
|
+
|
141
|
+
finalize_current_item()
|
142
|
+
return {
|
143
|
+
"summary": summary,
|
144
|
+
"args": args,
|
145
|
+
"returns": returns,
|
146
|
+
"raises": raises,
|
147
|
+
"tags": tags,
|
148
|
+
}
|
149
|
+
|
150
|
+
|
151
|
+
docstring_example = """
|
152
|
+
Starts a crawl job for a given URL using Firecrawl. Returns the job ID immediately.
|
153
|
+
|
154
|
+
Args:
|
155
|
+
url: The starting URL for the crawl.
|
156
|
+
It can be a very long url that spans multiple lines if needed.
|
157
|
+
params: Optional dictionary of parameters to customize the crawl.
|
158
|
+
See API docs for details.
|
159
|
+
idempotency_key: Optional unique key to prevent duplicate jobs.
|
160
|
+
|
161
|
+
Returns:
|
162
|
+
A dictionary containing the job initiation response on success,
|
163
|
+
or a string containing an error message on failure. This description
|
164
|
+
can also span multiple lines.
|
165
|
+
|
166
|
+
Raises:
|
167
|
+
ValueError: If the URL is invalid.
|
168
|
+
requests.exceptions.ConnectionError: If connection fails.
|
169
|
+
|
170
|
+
Tags:
|
171
|
+
crawl, async_job, start, api, long_tag_example , another
|
172
|
+
, final_tag
|
173
|
+
"""
|
174
|
+
|
175
|
+
if __name__ == "__main__":
|
176
|
+
parsed = parse_docstring(docstring_example)
|
177
|
+
import json
|
178
|
+
|
179
|
+
print(json.dumps(parsed, indent=4))
|
@@ -7,62 +7,72 @@ from universal_mcp.applications import app_from_slug
|
|
7
7
|
def discover_available_app_slugs():
|
8
8
|
apps_dir = Path(__file__).resolve().parent.parent / "applications"
|
9
9
|
app_slugs = []
|
10
|
-
|
10
|
+
|
11
11
|
for item in apps_dir.iterdir():
|
12
|
-
if not item.is_dir() or item.name.startswith(
|
12
|
+
if not item.is_dir() or item.name.startswith("_"):
|
13
13
|
continue
|
14
|
-
|
14
|
+
|
15
15
|
if (item / "app.py").exists():
|
16
16
|
slug = item.name.replace("_", "-")
|
17
17
|
app_slugs.append(slug)
|
18
|
-
|
18
|
+
|
19
19
|
return app_slugs
|
20
20
|
|
21
|
+
|
21
22
|
def extract_app_tools(app_slugs):
|
22
23
|
all_apps_tools = []
|
23
|
-
|
24
|
+
|
24
25
|
for slug in app_slugs:
|
25
26
|
try:
|
26
27
|
print(f"Loading app: {slug}")
|
27
28
|
app_class = app_from_slug(slug)
|
28
|
-
|
29
|
+
|
29
30
|
app_instance = app_class(integration=None)
|
30
|
-
|
31
|
+
|
31
32
|
tools = app_instance.list_tools()
|
32
|
-
|
33
|
+
|
33
34
|
for tool in tools:
|
34
35
|
tool_name = tool.__name__
|
35
|
-
description =
|
36
|
-
|
37
|
-
|
38
|
-
"
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
36
|
+
description = (
|
37
|
+
tool.__doc__.strip().split("\n")[0]
|
38
|
+
if tool.__doc__
|
39
|
+
else "No description"
|
40
|
+
)
|
41
|
+
|
42
|
+
all_apps_tools.append(
|
43
|
+
{
|
44
|
+
"app_name": slug,
|
45
|
+
"tool_name": tool_name,
|
46
|
+
"description": description,
|
47
|
+
}
|
48
|
+
)
|
49
|
+
|
43
50
|
except Exception as e:
|
44
51
|
print(f"Error loading app {slug}: {e}")
|
45
|
-
|
52
|
+
|
46
53
|
return all_apps_tools
|
47
54
|
|
55
|
+
|
48
56
|
def write_to_csv(app_tools, output_file="app_tools.csv"):
|
49
57
|
fieldnames = ["app_name", "tool_name", "description"]
|
50
|
-
|
51
|
-
with open(output_file,
|
58
|
+
|
59
|
+
with open(output_file, "w", newline="") as csvfile:
|
52
60
|
writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
|
53
61
|
writer.writeheader()
|
54
62
|
writer.writerows(app_tools)
|
55
|
-
|
63
|
+
|
56
64
|
print(f"CSV file created: {output_file}")
|
57
65
|
|
66
|
+
|
58
67
|
def main():
|
59
68
|
app_slugs = discover_available_app_slugs()
|
60
69
|
print(f"Found {len(app_slugs)} app slugs: {', '.join(app_slugs)}")
|
61
|
-
|
70
|
+
|
62
71
|
app_tools = extract_app_tools(app_slugs)
|
63
72
|
print(f"Extracted {len(app_tools)} tools from all apps")
|
64
|
-
|
73
|
+
|
65
74
|
write_to_csv(app_tools)
|
66
75
|
|
76
|
+
|
67
77
|
if __name__ == "__main__":
|
68
|
-
main()
|
78
|
+
main()
|
@@ -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")
|