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.
Files changed (71) hide show
  1. universal_mcp/applications/__init__.py +7 -2
  2. universal_mcp/applications/ahrefs/README.md +76 -0
  3. universal_mcp/applications/ahrefs/__init__.py +0 -0
  4. universal_mcp/applications/ahrefs/app.py +2291 -0
  5. universal_mcp/applications/application.py +191 -87
  6. universal_mcp/applications/cal_com_v2/README.md +175 -0
  7. universal_mcp/applications/cal_com_v2/__init__.py +0 -0
  8. universal_mcp/applications/cal_com_v2/app.py +5390 -0
  9. universal_mcp/applications/calendly/app.py +0 -12
  10. universal_mcp/applications/clickup/README.md +160 -0
  11. universal_mcp/applications/clickup/__init__.py +0 -0
  12. universal_mcp/applications/clickup/app.py +5009 -0
  13. universal_mcp/applications/coda/app.py +0 -33
  14. universal_mcp/applications/e2b/app.py +2 -28
  15. universal_mcp/applications/falai/README.md +42 -0
  16. universal_mcp/applications/falai/__init__.py +0 -0
  17. universal_mcp/applications/falai/app.py +332 -0
  18. universal_mcp/applications/figma/README.md +74 -0
  19. universal_mcp/applications/figma/__init__.py +0 -0
  20. universal_mcp/applications/figma/app.py +1261 -0
  21. universal_mcp/applications/firecrawl/app.py +2 -32
  22. universal_mcp/applications/gong/README.md +88 -0
  23. universal_mcp/applications/gong/__init__.py +0 -0
  24. universal_mcp/applications/gong/app.py +2297 -0
  25. universal_mcp/applications/google_calendar/app.py +0 -11
  26. universal_mcp/applications/google_docs/app.py +0 -18
  27. universal_mcp/applications/google_drive/app.py +0 -17
  28. universal_mcp/applications/google_mail/app.py +0 -16
  29. universal_mcp/applications/google_sheet/app.py +0 -18
  30. universal_mcp/applications/hashnode/app.py +81 -0
  31. universal_mcp/applications/hashnode/prompt.md +23 -0
  32. universal_mcp/applications/heygen/README.md +69 -0
  33. universal_mcp/applications/heygen/__init__.py +0 -0
  34. universal_mcp/applications/heygen/app.py +956 -0
  35. universal_mcp/applications/mailchimp/README.md +306 -0
  36. universal_mcp/applications/mailchimp/__init__.py +0 -0
  37. universal_mcp/applications/mailchimp/app.py +10937 -0
  38. universal_mcp/applications/markitdown/app.py +2 -2
  39. universal_mcp/applications/perplexity/app.py +0 -35
  40. universal_mcp/applications/replicate/README.md +65 -0
  41. universal_mcp/applications/replicate/__init__.py +0 -0
  42. universal_mcp/applications/replicate/app.py +980 -0
  43. universal_mcp/applications/resend/app.py +0 -18
  44. universal_mcp/applications/retell_ai/README.md +46 -0
  45. universal_mcp/applications/retell_ai/__init__.py +0 -0
  46. universal_mcp/applications/retell_ai/app.py +333 -0
  47. universal_mcp/applications/rocketlane/README.md +42 -0
  48. universal_mcp/applications/rocketlane/__init__.py +0 -0
  49. universal_mcp/applications/rocketlane/app.py +194 -0
  50. universal_mcp/applications/serpapi/app.py +2 -28
  51. universal_mcp/applications/spotify/README.md +116 -0
  52. universal_mcp/applications/spotify/__init__.py +0 -0
  53. universal_mcp/applications/spotify/app.py +2526 -0
  54. universal_mcp/applications/supabase/README.md +112 -0
  55. universal_mcp/applications/supabase/__init__.py +0 -0
  56. universal_mcp/applications/supabase/app.py +2970 -0
  57. universal_mcp/applications/tavily/app.py +0 -20
  58. universal_mcp/applications/wrike/app.py +0 -12
  59. universal_mcp/applications/youtube/app.py +0 -18
  60. universal_mcp/integrations/agentr.py +27 -4
  61. universal_mcp/integrations/integration.py +14 -6
  62. universal_mcp/servers/server.py +53 -6
  63. universal_mcp/stores/store.py +6 -0
  64. universal_mcp/tools/tools.py +2 -2
  65. universal_mcp/utils/docstring_parser.py +192 -94
  66. universal_mcp/utils/installation.py +199 -8
  67. {universal_mcp-0.1.8rc3.dist-info → universal_mcp-0.1.9.dist-info}/METADATA +6 -1
  68. universal_mcp-0.1.9.dist-info/RECORD +116 -0
  69. universal_mcp-0.1.8rc3.dist-info/RECORD +0 -75
  70. {universal_mcp-0.1.8rc3.dist-info → universal_mcp-0.1.9.dist-info}/WHEEL +0 -0
  71. {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 standard Python docstring into summary, args, returns, raises, and tags.
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 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.
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 = 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
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
- """Helper function to finalize the currently parsed item."""
37
- nonlocal returns, tags # Allow modification of outer scope variables
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
- args[current_key] = desc
53
+ if desc:
54
+ args[current_key] = desc
41
55
  elif current_section == "raises" and current_key:
42
- raises[current_key] = desc
56
+ if desc:
57
+ raises[current_key] = desc
43
58
  elif current_section == "returns":
44
59
  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(" "))
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
- section_line = stripped_line.lower()
55
- is_new_section_header = False
56
- new_section_type = None
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 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 (
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
- new_section_type = "other"
83
- is_new_section_header = True
110
+ section_type = "other"
84
111
 
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)
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
- finalize_previous = True
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
- if finalize_previous:
179
+ # If finalizing the previous item/section
180
+ if should_finalize_previous:
104
181
  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
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
- current_key and original_indentation > 0
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 == "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)
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. Returns the job ID immediately.
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
- Raises:
264
+ Raises:
167
265
  ValueError: If the URL is invalid.
168
- requests.exceptions.ConnectionError: If connection fails.
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 install_windsurf() -> None:
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("[yellow]Windsurf installation not yet implemented[/yellow]")
105
- pass
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.8rc3
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