universal-mcp 0.1.8rc2__py3-none-any.whl → 0.1.8rc4__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/__init__.py +0 -0
- universal_mcp/applications/ahrefs/app.py +2291 -0
- universal_mcp/applications/application.py +94 -5
- universal_mcp/applications/calendly/app.py +412 -171
- 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 +8 -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 +3 -33
- universal_mcp/applications/github/app.py +41 -42
- universal_mcp/applications/google_calendar/app.py +20 -31
- universal_mcp/applications/google_docs/app.py +21 -46
- universal_mcp/applications/google_drive/app.py +53 -76
- universal_mcp/applications/google_mail/app.py +40 -56
- universal_mcp/applications/google_sheet/app.py +43 -68
- universal_mcp/applications/markitdown/app.py +4 -4
- universal_mcp/applications/notion/app.py +93 -83
- universal_mcp/applications/perplexity/app.py +4 -38
- universal_mcp/applications/reddit/app.py +32 -32
- universal_mcp/applications/resend/app.py +4 -22
- universal_mcp/applications/serpapi/app.py +6 -32
- universal_mcp/applications/tavily/app.py +4 -24
- universal_mcp/applications/wrike/app.py +565 -237
- universal_mcp/applications/youtube/app.py +625 -183
- universal_mcp/applications/zenquotes/app.py +3 -3
- universal_mcp/exceptions.py +1 -0
- universal_mcp/integrations/__init__.py +11 -2
- universal_mcp/integrations/agentr.py +27 -4
- universal_mcp/integrations/integration.py +14 -6
- universal_mcp/logger.py +3 -56
- universal_mcp/servers/__init__.py +2 -1
- universal_mcp/servers/server.py +73 -77
- universal_mcp/stores/store.py +5 -3
- universal_mcp/tools/__init__.py +1 -1
- universal_mcp/tools/adapters.py +4 -1
- universal_mcp/tools/func_metadata.py +5 -6
- universal_mcp/tools/tools.py +108 -51
- universal_mcp/utils/docgen.py +121 -69
- universal_mcp/utils/docstring_parser.py +44 -21
- universal_mcp/utils/dump_app_tools.py +33 -23
- universal_mcp/utils/installation.py +199 -8
- universal_mcp/utils/openapi.py +121 -47
- {universal_mcp-0.1.8rc2.dist-info → universal_mcp-0.1.8rc4.dist-info}/METADATA +2 -2
- universal_mcp-0.1.8rc4.dist-info/RECORD +81 -0
- universal_mcp-0.1.8rc2.dist-info/RECORD +0 -71
- {universal_mcp-0.1.8rc2.dist-info → universal_mcp-0.1.8rc4.dist-info}/WHEEL +0 -0
- {universal_mcp-0.1.8rc2.dist-info → universal_mcp-0.1.8rc4.dist-info}/entry_points.txt +0 -0
universal_mcp/utils/docgen.py
CHANGED
@@ -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(
|
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(
|
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(
|
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
|
-
|
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", [])
|
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 = {
|
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
|
-
|
298
|
-
|
299
|
-
elif docstring.args.get(
|
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[
|
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
|
-
|
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 (
|
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
|
317
|
-
|
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
|
-
|
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(
|
354
|
-
|
355
|
-
|
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,
|
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 = " "
|
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):
|
382
|
-
|
383
|
-
|
384
|
-
|
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(
|
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():
|
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 =
|
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
|
436
|
+
break # Exit this loop, we'll process func_node.body instead
|
409
437
|
cleaned_body_parts = []
|
410
|
-
start_stmt_index =
|
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
|
-
|
448
|
+
stmt_node = func_node.body[i]
|
414
449
|
|
415
|
-
|
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
|
-
|
418
|
-
|
419
|
-
|
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
|
-
|
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
|
-
|
427
|
-
|
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(
|
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
|
-
|
436
|
-
|
437
|
-
|
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
|
-
|
440
|
-
|
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
|
-
|
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(
|
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 = []
|
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
|
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:
|
47
|
-
tags = [tag.strip() for tag in desc.split(
|
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(
|
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 (
|
74
|
-
|
75
|
-
|
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
|
-
|
90
|
+
finalize_previous = True
|
83
91
|
elif current_section in ["returns", "tags"] and current_desc_lines:
|
84
|
-
|
85
|
-
|
92
|
+
if original_indentation == 0 and stripped_line:
|
93
|
+
finalize_previous = True
|
86
94
|
# SIM102 applied: Combine nested if/elif
|
87
|
-
elif (
|
88
|
-
|
89
|
-
|
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
|
-
|
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()]
|
114
|
-
elif
|
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
|
-
|
123
|
-
|
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 {
|
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 =
|
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()
|