universal-mcp 0.1.8rc2__py3-none-any.whl → 0.1.8rc3__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 (45) hide show
  1. universal_mcp/__init__.py +0 -2
  2. universal_mcp/analytics.py +75 -0
  3. universal_mcp/applications/application.py +27 -5
  4. universal_mcp/applications/calendly/app.py +413 -160
  5. universal_mcp/applications/coda/README.md +133 -0
  6. universal_mcp/applications/coda/__init__.py +0 -0
  7. universal_mcp/applications/coda/app.py +3704 -0
  8. universal_mcp/applications/e2b/app.py +6 -7
  9. universal_mcp/applications/firecrawl/app.py +1 -1
  10. universal_mcp/applications/github/app.py +41 -42
  11. universal_mcp/applications/google_calendar/app.py +20 -20
  12. universal_mcp/applications/google_docs/app.py +22 -29
  13. universal_mcp/applications/google_drive/app.py +53 -59
  14. universal_mcp/applications/google_mail/app.py +40 -40
  15. universal_mcp/applications/google_sheet/app.py +44 -51
  16. universal_mcp/applications/markitdown/app.py +4 -4
  17. universal_mcp/applications/notion/app.py +93 -83
  18. universal_mcp/applications/perplexity/app.py +5 -5
  19. universal_mcp/applications/reddit/app.py +32 -32
  20. universal_mcp/applications/resend/app.py +4 -4
  21. universal_mcp/applications/serpapi/app.py +4 -4
  22. universal_mcp/applications/tavily/app.py +4 -4
  23. universal_mcp/applications/wrike/app.py +566 -226
  24. universal_mcp/applications/youtube/app.py +626 -166
  25. universal_mcp/applications/zenquotes/app.py +3 -3
  26. universal_mcp/exceptions.py +1 -0
  27. universal_mcp/integrations/__init__.py +11 -2
  28. universal_mcp/integrations/integration.py +2 -2
  29. universal_mcp/logger.py +3 -56
  30. universal_mcp/servers/__init__.py +2 -1
  31. universal_mcp/servers/server.py +76 -77
  32. universal_mcp/stores/store.py +5 -3
  33. universal_mcp/tools/__init__.py +1 -1
  34. universal_mcp/tools/adapters.py +4 -1
  35. universal_mcp/tools/func_metadata.py +5 -6
  36. universal_mcp/tools/tools.py +108 -51
  37. universal_mcp/utils/docgen.py +121 -69
  38. universal_mcp/utils/docstring_parser.py +44 -21
  39. universal_mcp/utils/dump_app_tools.py +33 -23
  40. universal_mcp/utils/openapi.py +121 -47
  41. {universal_mcp-0.1.8rc2.dist-info → universal_mcp-0.1.8rc3.dist-info}/METADATA +2 -2
  42. universal_mcp-0.1.8rc3.dist-info/RECORD +75 -0
  43. universal_mcp-0.1.8rc2.dist-info/RECORD +0 -71
  44. {universal_mcp-0.1.8rc2.dist-info → universal_mcp-0.1.8rc3.dist-info}/WHEEL +0 -0
  45. {universal_mcp-0.1.8rc2.dist-info → universal_mcp-0.1.8rc3.dist-info}/entry_points.txt +0 -0
@@ -7,12 +7,12 @@ using LLMs with structured output
7
7
  import ast
8
8
  import json
9
9
  import os
10
+ import re
10
11
  import sys
11
12
  import textwrap
12
13
  import traceback
13
14
 
14
15
  import litellm
15
- import re
16
16
  from pydantic import BaseModel, Field
17
17
 
18
18
 
@@ -28,11 +28,11 @@ class DocstringOutput(BaseModel):
28
28
  returns: str = Field(description="Description of what the function returns")
29
29
  raises: dict[str, str] = Field(
30
30
  default_factory=dict,
31
- description="Dictionary mapping potential exception types/reasons to their descriptions"
31
+ description="Dictionary mapping potential exception types/reasons to their descriptions",
32
32
  )
33
33
  tags: list[str] = Field(
34
34
  default_factory=list,
35
- description="List of relevant tags for the function (e.g., action, job type, async status, importance)"
35
+ description="List of relevant tags for the function (e.g., action, job type, async status, importance)",
36
36
  )
37
37
 
38
38
 
@@ -62,7 +62,7 @@ class FunctionExtractor(ast.NodeVisitor):
62
62
  def visit_FunctionDef(self, node: ast.FunctionDef):
63
63
  """Visits a regular function definition and collects it if not excluded."""
64
64
  # Add the exclusion logic here
65
- if not node.name.startswith('_') and node.name != 'list_tools':
65
+ if not node.name.startswith("_") and node.name != "list_tools":
66
66
  source_code = self._get_source_segment(node)
67
67
  if source_code:
68
68
  self.functions.append((node.name, source_code))
@@ -72,7 +72,7 @@ class FunctionExtractor(ast.NodeVisitor):
72
72
  def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef):
73
73
  """Visits an asynchronous function definition and collects it if not excluded."""
74
74
  # Add the exclusion logic here
75
- if not node.name.startswith('_') and node.name != 'list_tools':
75
+ if not node.name.startswith("_") and node.name != "list_tools":
76
76
  source_code = self._get_source_segment(node)
77
77
  if source_code:
78
78
  self.functions.append((node.name, source_code))
@@ -136,13 +136,13 @@ def extract_functions_from_script(file_path: str) -> list[tuple[str, str]]:
136
136
 
137
137
  def extract_json_from_text(text):
138
138
  """Extract valid JSON from text that might contain additional content.
139
-
139
+
140
140
  Args:
141
141
  text: Raw text response from the model
142
-
142
+
143
143
  Returns:
144
144
  Dict containing the extracted JSON data
145
-
145
+
146
146
  Raises:
147
147
  ValueError: If no valid JSON could be extracted
148
148
  """
@@ -151,28 +151,28 @@ def extract_json_from_text(text):
151
151
  if json_match:
152
152
  try:
153
153
  return json.loads(json_match.group(1))
154
- except:
154
+ except json.JSONDecodeError:
155
155
  pass
156
156
 
157
157
  # Try to find the first { and last } for a complete JSON object
158
158
  try:
159
- start = text.find('{')
159
+ start = text.find("{")
160
160
  if start >= 0:
161
161
  brace_count = 0
162
162
  for i in range(start, len(text)):
163
- if text[i] == '{':
163
+ if text[i] == "{":
164
164
  brace_count += 1
165
- elif text[i] == '}':
165
+ elif text[i] == "}":
166
166
  brace_count -= 1
167
167
  if brace_count == 0:
168
- return json.loads(text[start:i+1])
169
- except:
168
+ return json.loads(text[start : i + 1])
169
+ except json.JSONDecodeError:
170
170
  pass
171
-
171
+
172
172
  try:
173
173
  return json.loads(text)
174
- except:
175
- raise ValueError("Could not extract valid JSON from the response")
174
+ except json.JSONDecodeError as e:
175
+ raise ValueError("Could not extract valid JSON from the response") from e
176
176
 
177
177
 
178
178
  def generate_docstring(
@@ -235,28 +235,29 @@ def generate_docstring(
235
235
 
236
236
  response_text = response.choices[0].message.content
237
237
 
238
-
239
238
  try:
240
239
  parsed_data = extract_json_from_text(response_text)
241
240
  except ValueError as e:
242
241
  print(f"JSON extraction failed: {e}")
243
- print(f"Raw response: {response_text[:100]}...") # Log first 100 chars for debugging
242
+ print(
243
+ f"Raw response: {response_text[:100]}..."
244
+ ) # Log first 100 chars for debugging
244
245
  # Return a default structure if extraction fails
245
246
  return DocstringOutput(
246
247
  summary="Failed to extract docstring information",
247
248
  args={"None": "This function takes no arguments"},
248
- returns="Unknown return value"
249
+ returns="Unknown return value",
249
250
  )
250
251
  model_args = parsed_data.get("args")
251
252
  if not model_args:
252
- parsed_data["args"] = {"None": "This function takes no arguments"}
253
+ parsed_data["args"] = {"None": "This function takes no arguments"}
253
254
 
254
255
  return DocstringOutput(
255
256
  summary=parsed_data.get("summary", "No documentation available"),
256
257
  args=parsed_data.get("args", {"None": "This function takes no arguments"}),
257
258
  returns=parsed_data.get("returns", "None"),
258
259
  raises=parsed_data.get("raises", {}),
259
- tags=parsed_data.get("tags", []) # Get tags, default to empty list
260
+ tags=parsed_data.get("tags", []), # Get tags, default to empty list
260
261
  )
261
262
 
262
263
  except Exception as e:
@@ -267,9 +268,10 @@ def generate_docstring(
267
268
  args={"None": "This function takes no arguments"},
268
269
  returns="None",
269
270
  raises={},
270
- tags=["generation-error"]
271
+ tags=["generation-error"],
271
272
  )
272
273
 
274
+
273
275
  def format_docstring(docstring: DocstringOutput) -> str:
274
276
  """
275
277
  Format a DocstringOutput object into the content string for a docstring.
@@ -289,23 +291,29 @@ def format_docstring(docstring: DocstringOutput) -> str:
289
291
  if summary:
290
292
  parts.append(summary)
291
293
 
292
- filtered_args = {name: desc for name, desc in docstring.args.items() if name not in ('self', 'cls')}
294
+ filtered_args = {
295
+ name: desc
296
+ for name, desc in docstring.args.items()
297
+ if name not in ("self", "cls")
298
+ }
293
299
  args_lines = []
294
300
  if filtered_args:
295
301
  args_lines.append("Args:")
296
302
  for arg_name, arg_desc in filtered_args.items():
297
- arg_desc_cleaned = arg_desc.strip()
298
- args_lines.append(f" {arg_name}: {arg_desc_cleaned}")
299
- elif docstring.args.get('None'): # Include the 'None' placeholder if it was generated
303
+ arg_desc_cleaned = arg_desc.strip()
304
+ args_lines.append(f" {arg_name}: {arg_desc_cleaned}")
305
+ elif docstring.args.get(
306
+ "None"
307
+ ): # Include the 'None' placeholder if it was generated
300
308
  args_lines.append("Args:")
301
- none_desc_cleaned = docstring.args['None'].strip()
309
+ none_desc_cleaned = docstring.args["None"].strip()
302
310
  args_lines.append(f" None: {none_desc_cleaned}")
303
311
 
304
312
  if args_lines:
305
- parts.append("\n".join(args_lines))
313
+ parts.append("\n".join(args_lines))
306
314
 
307
315
  returns_desc_cleaned = docstring.returns.strip()
308
- if returns_desc_cleaned and returns_desc_cleaned.lower() not in ('none', ''):
316
+ if returns_desc_cleaned and returns_desc_cleaned.lower() not in ("none", ""):
309
317
  parts.append(f"Returns:\n {returns_desc_cleaned}")
310
318
 
311
319
  raises_lines = []
@@ -313,10 +321,14 @@ def format_docstring(docstring: DocstringOutput) -> str:
313
321
  raises_lines.append("Raises:")
314
322
  for exception_type, exception_desc in docstring.raises.items():
315
323
  exception_desc_cleaned = exception_desc.strip()
316
- if exception_type.strip() and exception_desc_cleaned: # Ensure type and desc are not empty
317
- raises_lines.append(f" {exception_type.strip()}: {exception_desc_cleaned}")
324
+ if (
325
+ exception_type.strip() and exception_desc_cleaned
326
+ ): # Ensure type and desc are not empty
327
+ raises_lines.append(
328
+ f" {exception_type.strip()}: {exception_desc_cleaned}"
329
+ )
318
330
  if raises_lines:
319
- parts.append("\n".join(raises_lines))
331
+ parts.append("\n".join(raises_lines))
320
332
 
321
333
  cleaned_tags = [tag.strip() for tag in docstring.tags if tag and tag.strip()]
322
334
  if cleaned_tags:
@@ -325,6 +337,7 @@ def format_docstring(docstring: DocstringOutput) -> str:
325
337
 
326
338
  return "\n\n".join(parts)
327
339
 
340
+
328
341
  def insert_docstring_into_function(function_code: str, docstring: str) -> str:
329
342
  """
330
343
  Insert a docstring into a function's code, replacing an existing one if present
@@ -350,43 +363,51 @@ def insert_docstring_into_function(function_code: str, docstring: str) -> str:
350
363
  lines = function_code.splitlines(keepends=True)
351
364
 
352
365
  tree = ast.parse(function_code)
353
- if not tree.body or not isinstance(tree.body[0], ast.FunctionDef | ast.AsyncFunctionDef):
354
- print("Warning: Could not parse function definition from code snippet. Returning original code.", file=sys.stderr)
355
- return function_code # Return original code if parsing fails or isn't a function
366
+ if not tree.body or not isinstance(
367
+ tree.body[0], ast.FunctionDef | ast.AsyncFunctionDef
368
+ ):
369
+ print(
370
+ "Warning: Could not parse function definition from code snippet. Returning original code.",
371
+ file=sys.stderr,
372
+ )
373
+ return function_code # Return original code if parsing fails or isn't a function
356
374
 
357
375
  func_node = tree.body[0]
358
- func_name = getattr(func_node, 'name', 'unknown_function')
376
+ func_name = getattr(func_node, "name", "unknown_function")
359
377
 
360
378
  insert_idx = func_node.end_lineno
361
379
 
362
380
  if func_node.body:
363
381
  insert_idx = func_node.body[0].lineno - 1
364
382
 
365
- body_indent = " " # Default indentation (PEP 8)
383
+ body_indent = " " # Default indentation (PEP 8)
366
384
 
367
385
  indent_source_idx = insert_idx
368
386
  actual_first_body_line_idx = -1
369
387
  for i in range(indent_source_idx, len(lines)):
370
388
  line = lines[i]
371
389
  stripped = line.lstrip()
372
- if stripped and not stripped.startswith('#'):
390
+ if stripped and not stripped.startswith("#"):
373
391
  actual_first_body_line_idx = i
374
392
  break
375
393
 
376
394
  # If a meaningful line was found at or after insertion point, use its indentation
377
395
  if actual_first_body_line_idx != -1:
378
396
  body_line = lines[actual_first_body_line_idx]
379
- body_indent = body_line[:len(body_line) - len(body_line.lstrip())]
397
+ body_indent = body_line[: len(body_line) - len(body_line.lstrip())]
380
398
  else:
381
- if func_node.lineno - 1 < len(lines): # Ensure def line exists
382
- def_line = lines[func_node.lineno - 1]
383
- def_line_indent = def_line[:len(def_line) - len(def_line.lstrip())]
384
- body_indent = def_line_indent + " " # Standard 4 spaces relative indent
385
-
399
+ if func_node.lineno - 1 < len(lines): # Ensure def line exists
400
+ def_line = lines[func_node.lineno - 1]
401
+ def_line_indent = def_line[: len(def_line) - len(def_line.lstrip())]
402
+ body_indent = (
403
+ def_line_indent + " "
404
+ ) # Standard 4 spaces relative indent
386
405
 
387
406
  # Format the new docstring lines with the calculated indentation
388
407
  new_docstring_lines_formatted = [f'{body_indent}"""\n']
389
- new_docstring_lines_formatted.extend([f"{body_indent}{line}\n" for line in docstring.splitlines()])
408
+ new_docstring_lines_formatted.extend(
409
+ [f"{body_indent}{line}\n" for line in docstring.splitlines()]
410
+ )
390
411
  new_docstring_lines_formatted.append(f'{body_indent}"""\n')
391
412
 
392
413
  output_lines = []
@@ -398,62 +419,93 @@ def insert_docstring_into_function(function_code: str, docstring: str) -> str:
398
419
 
399
420
  remaining_body_code = "".join(remaining_body_lines)
400
421
 
401
- if remaining_body_code.strip(): # Only parse if there's non-whitespace content
422
+ if remaining_body_code.strip(): # Only parse if there's non-whitespace content
402
423
  try:
403
424
  dummy_code = f"def _dummy_func():\n{textwrap.indent(remaining_body_code, body_indent)}"
404
425
  dummy_tree = ast.parse(dummy_code)
405
- dummy_body_statements = dummy_tree.body[0].body if dummy_tree.body and isinstance(dummy_tree.body[0], ast.FunctionDef | ast.AsyncFunctionDef) else []
426
+ dummy_body_statements = (
427
+ dummy_tree.body[0].body
428
+ if dummy_tree.body
429
+ and isinstance(
430
+ dummy_tree.body[0], ast.FunctionDef | ast.AsyncFunctionDef
431
+ )
432
+ else []
433
+ )
406
434
  cleaned_body_parts = []
407
435
  for _node in dummy_body_statements:
408
- break # Exit this loop, we'll process func_node.body instead
436
+ break # Exit this loop, we'll process func_node.body instead
409
437
  cleaned_body_parts = []
410
- start_stmt_index = 1 if func_node.body and isinstance(func_node.body[0], ast.Expr) and isinstance(func_node.body[0].value, ast.Constant) and isinstance(func_node.body[0].value.value, str) else 0
438
+ start_stmt_index = (
439
+ 1
440
+ if func_node.body
441
+ and isinstance(func_node.body[0], ast.Expr)
442
+ and isinstance(func_node.body[0].value, ast.Constant)
443
+ and isinstance(func_node.body[0].value.value, str)
444
+ else 0
445
+ )
411
446
 
412
447
  for i in range(start_stmt_index, len(func_node.body)):
413
- stmt_node = func_node.body[i]
448
+ stmt_node = func_node.body[i]
414
449
 
415
- is_just_string_stmt = isinstance(stmt_node, ast.Expr) and isinstance(stmt_node.value, ast.Constant) and isinstance(stmt_node.value.value, str)
450
+ is_just_string_stmt = (
451
+ isinstance(stmt_node, ast.Expr)
452
+ and isinstance(stmt_node.value, ast.Constant)
453
+ and isinstance(stmt_node.value.value, str)
454
+ )
416
455
 
417
- if not is_just_string_stmt:
418
- stmt_start_idx = stmt_node.lineno - 1
419
- stmt_end_idx = stmt_node.end_lineno - 1 # Inclusive end line index
456
+ if not is_just_string_stmt:
457
+ stmt_start_idx = stmt_node.lineno - 1
458
+ stmt_end_idx = (
459
+ stmt_node.end_lineno - 1
460
+ ) # Inclusive end line index
420
461
 
421
- cleaned_body_parts.extend(lines[stmt_start_idx : stmt_end_idx + 1])
462
+ cleaned_body_parts.extend(
463
+ lines[stmt_start_idx : stmt_end_idx + 1]
464
+ )
422
465
 
423
466
  if func_node.body:
424
467
  last_stmt_end_idx = func_node.body[-1].end_lineno - 1
425
- for line in lines[last_stmt_end_idx + 1:]:
426
- if line.strip():
427
- cleaned_body_parts.append(line)
468
+ for line in lines[last_stmt_end_idx + 1 :]:
469
+ if line.strip():
470
+ cleaned_body_parts.append(line)
428
471
  cleaned_body_lines = cleaned_body_parts
429
472
 
430
473
  except SyntaxError as parse_e:
431
- print(f"WARNING: Could not parse function body for cleaning, keeping all body lines: {parse_e}", file=sys.stderr)
474
+ print(
475
+ f"WARNING: Could not parse function body for cleaning, keeping all body lines: {parse_e}",
476
+ file=sys.stderr,
477
+ )
432
478
  traceback.print_exc(file=sys.stderr)
433
479
  cleaned_body_lines = remaining_body_lines
434
480
  except Exception as other_e:
435
- print(f"WARNING: Unexpected error processing function body for cleaning, keeping all body lines: {other_e}", file=sys.stderr)
436
- traceback.print_exc(file=sys.stderr)
437
- cleaned_body_lines = remaining_body_lines
481
+ print(
482
+ f"WARNING: Unexpected error processing function body for cleaning, keeping all body lines: {other_e}",
483
+ file=sys.stderr,
484
+ )
485
+ traceback.print_exc(file=sys.stderr)
486
+ cleaned_body_lines = remaining_body_lines
438
487
  else:
439
- cleaned_body_lines = []
440
- output_lines.extend(lines[func_node.end_lineno:])
488
+ cleaned_body_lines = []
489
+ output_lines.extend(lines[func_node.end_lineno :])
441
490
 
442
491
  if func_node.body or not remaining_body_code.strip():
443
- output_lines.extend(cleaned_body_lines)
492
+ output_lines.extend(cleaned_body_lines)
444
493
 
445
494
  final_code = "".join(output_lines)
446
495
  ast.parse(final_code)
447
496
  return final_code
448
497
 
449
498
  except SyntaxError as e:
450
- print(f"WARNING: Generated code snippet for '{func_name}' has syntax error: {e}", file=sys.stderr)
499
+ print(
500
+ f"WARNING: Generated code snippet for '{func_name}' has syntax error: {e}",
501
+ file=sys.stderr,
502
+ )
451
503
  traceback.print_exc(file=sys.stderr)
452
504
  return function_code
453
505
  except Exception as e:
454
506
  print(f"Error processing function snippet for insertion: {e}", file=sys.stderr)
455
507
  traceback.print_exc(file=sys.stderr)
456
-
508
+
457
509
  return function_code
458
510
 
459
511
 
@@ -508,7 +560,7 @@ def process_file(file_path: str, model: str = "perplexity/sonar-pro") -> int:
508
560
  f.write(updated_content)
509
561
  print(f"Updated {count} functions in {file_path}")
510
562
  else:
511
- print(updated_function, "formatted docstring",formatted_docstring)
563
+ print(updated_function, "formatted docstring", formatted_docstring)
512
564
  print(f"No changes made to {file_path}")
513
565
 
514
566
  return count
@@ -29,12 +29,12 @@ def parse_docstring(docstring: str | None) -> dict[str, Any]:
29
29
  tags: list[str] = [] # Final list of parsed tags
30
30
  current_section = None
31
31
  current_key = None
32
- current_desc_lines = [] # Accumulator for multi-line descriptions/tag content
32
+ current_desc_lines = [] # Accumulator for multi-line descriptions/tag content
33
33
  key_pattern = re.compile(r"^\s*([\w\.]+)\s*(?:\(.*\))?:\s*(.*)")
34
34
 
35
35
  def finalize_current_item():
36
36
  """Helper function to finalize the currently parsed item."""
37
- nonlocal returns, tags # Allow modification of outer scope variables
37
+ nonlocal returns, tags # Allow modification of outer scope variables
38
38
  desc = " ".join(current_desc_lines).strip()
39
39
  if current_section == "args" and current_key:
40
40
  args[current_key] = desc
@@ -43,13 +43,13 @@ def parse_docstring(docstring: str | None) -> dict[str, Any]:
43
43
  elif current_section == "returns":
44
44
  returns = desc
45
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()]
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
48
 
49
49
  # B007 applied: Rename unused loop variable i to _
50
50
  for _, line in enumerate(lines[1:]):
51
51
  stripped_line = line.strip()
52
- original_indentation = len(line) - len(line.lstrip(' '))
52
+ original_indentation = len(line) - len(line.lstrip(" "))
53
53
 
54
54
  section_line = stripped_line.lower()
55
55
  is_new_section_header = False
@@ -65,35 +65,47 @@ def parse_docstring(docstring: str | None) -> dict[str, Any]:
65
65
  elif section_line.startswith(("raises ", "raises:", "errors:", "exceptions:")):
66
66
  new_section_type = "raises"
67
67
  is_new_section_header = True
68
- elif section_line.startswith(("tags:", "tags")): # Match "Tags:" or "Tags" potentially followed by content
68
+ elif section_line.startswith(
69
+ ("tags:", "tags")
70
+ ): # Match "Tags:" or "Tags" potentially followed by content
69
71
  new_section_type = "tags"
70
72
  is_new_section_header = True
71
73
  if ":" in stripped_line:
72
74
  header_content = stripped_line.split(":", 1)[1].strip()
73
- elif section_line.endswith(":") and section_line[:-1] in ("attributes", "see also", "example", "examples", "notes"):
74
- new_section_type = "other"
75
- is_new_section_header = True
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
76
84
 
77
85
  finalize_previous = False
78
86
  if is_new_section_header:
79
87
  finalize_previous = True
80
88
  elif current_section in ["args", "raises"] and current_key:
81
89
  if key_pattern.match(line) or (original_indentation == 0 and stripped_line):
82
- finalize_previous = True
90
+ finalize_previous = True
83
91
  elif current_section in ["returns", "tags"] and current_desc_lines:
84
- if original_indentation == 0 and stripped_line:
85
- finalize_previous = True
92
+ if original_indentation == 0 and stripped_line:
93
+ finalize_previous = True
86
94
  # SIM102 applied: Combine nested if/elif
87
- elif (not stripped_line and current_desc_lines and current_section in ["args", "raises", "returns", "tags"]
88
- and (current_section not in ["args", "raises"] or current_key)):
89
- finalize_previous = True
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
90
102
 
91
103
  if finalize_previous:
92
104
  finalize_current_item()
93
105
  current_key = None
94
106
  current_desc_lines = []
95
107
  if not is_new_section_header or new_section_type == "other":
96
- current_section = None
108
+ current_section = None
97
109
 
98
110
  if is_new_section_header and new_section_type != "other":
99
111
  current_section = new_section_type
@@ -110,8 +122,10 @@ def parse_docstring(docstring: str | None) -> dict[str, Any]:
110
122
  match = key_pattern.match(line)
111
123
  if match:
112
124
  current_key = match.group(1)
113
- current_desc_lines = [match.group(2).strip()] # Start new description
114
- elif current_key and original_indentation > 0: # Check for indentation for continuation
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
115
129
  current_desc_lines.append(stripped_line)
116
130
 
117
131
  elif current_section == "returns":
@@ -119,11 +133,19 @@ def parse_docstring(docstring: str | None) -> dict[str, Any]:
119
133
  current_desc_lines.append(stripped_line)
120
134
 
121
135
  elif current_section == "tags":
122
- if original_indentation > 0 or not current_desc_lines: # Indented or first line
123
- current_desc_lines.append(stripped_line)
136
+ if (
137
+ original_indentation > 0 or not current_desc_lines
138
+ ): # Indented or first line
139
+ current_desc_lines.append(stripped_line)
124
140
 
125
141
  finalize_current_item()
126
- return {"summary": summary, "args": args, "returns": returns, "raises": raises, "tags": tags}
142
+ return {
143
+ "summary": summary,
144
+ "args": args,
145
+ "returns": returns,
146
+ "raises": raises,
147
+ "tags": tags,
148
+ }
127
149
 
128
150
 
129
151
  docstring_example = """
@@ -153,4 +175,5 @@ docstring_example = """
153
175
  if __name__ == "__main__":
154
176
  parsed = parse_docstring(docstring_example)
155
177
  import json
178
+
156
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 = tool.__doc__.strip().split('\n')[0] if tool.__doc__ else "No description"
36
-
37
- all_apps_tools.append({
38
- "app_name": slug,
39
- "tool_name": tool_name,
40
- "description": description
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, 'w', newline='') as csvfile:
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()