universal-mcp 0.1.21rc2__py3-none-any.whl → 0.1.22rc4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- universal_mcp/applications/__init__.py +5 -1
- universal_mcp/integrations/integration.py +4 -8
- universal_mcp/servers/server.py +15 -31
- universal_mcp/tools/adapters.py +39 -3
- universal_mcp/tools/manager.py +122 -37
- universal_mcp/tools/tools.py +1 -1
- universal_mcp/utils/agentr.py +27 -13
- universal_mcp/utils/docstring_parser.py +18 -64
- universal_mcp/utils/openapi/api_splitter.py +250 -132
- universal_mcp/utils/openapi/openapi.py +137 -118
- universal_mcp/utils/openapi/preprocessor.py +272 -29
- universal_mcp/utils/testing.py +31 -0
- {universal_mcp-0.1.21rc2.dist-info → universal_mcp-0.1.22rc4.dist-info}/METADATA +2 -1
- {universal_mcp-0.1.21rc2.dist-info → universal_mcp-0.1.22rc4.dist-info}/RECORD +17 -16
- {universal_mcp-0.1.21rc2.dist-info → universal_mcp-0.1.22rc4.dist-info}/WHEEL +0 -0
- {universal_mcp-0.1.21rc2.dist-info → universal_mcp-0.1.22rc4.dist-info}/entry_points.txt +0 -0
- {universal_mcp-0.1.21rc2.dist-info → universal_mcp-0.1.22rc4.dist-info}/licenses/LICENSE +0 -0
@@ -225,10 +225,17 @@ def generate_description_llm(
|
|
225
225
|
if len(param_context_str) > 1000: # Limit context size
|
226
226
|
param_context_str = param_context_str[:1000] + "..."
|
227
227
|
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
228
|
+
current_description = context.get("current_description", None)
|
229
|
+
if current_description and isinstance(current_description, str) and current_description.strip():
|
230
|
+
user_prompt = f"""The current description for the API parameter named '{param_name}' located '{param_in}' for the '{method.upper()}' operation at path '{path_key}' is:\n'{current_description.strip()}'\n\nTask: Rewrite and enrich this description so it is clear, self-contained, and makes sense to a user. If the description is cut off, incomplete, or awkward, make it complete and natural. Ensure it is concise and under {MAX_DESCRIPTION_LENGTH} characters. Do not include any links, HTML, markdown, or any notes or comments about the character limit. Respond ONLY with the improved single-line description."""
|
231
|
+
fallback_text = (
|
232
|
+
f"[LLM could not generate description for parameter {param_name} in {method.upper()} {path_key}]"
|
233
|
+
)
|
234
|
+
else:
|
235
|
+
user_prompt = f"""Generate a clear, brief description for the API parameter named "{param_name}" located "{param_in}" for the "{method.upper()}" operation at path "{path_key}".\nContext (parameter details): {param_context_str}\nRespond ONLY with the *SINGLE LINE* description text."""
|
236
|
+
fallback_text = (
|
237
|
+
f"[LLM could not generate description for parameter {param_name} in {method.upper()} {path_key}]"
|
238
|
+
)
|
232
239
|
|
233
240
|
elif description_type == "api_description":
|
234
241
|
api_title = context.get("title", "Untitled API")
|
@@ -236,6 +243,24 @@ def generate_description_llm(
|
|
236
243
|
Respond ONLY with the description text."""
|
237
244
|
fallback_text = f"[LLM could not generate description for API '{api_title}']" # More specific fallback
|
238
245
|
|
246
|
+
elif description_type == "operation_id":
|
247
|
+
path_key = context.get("path_key", "unknown path")
|
248
|
+
method = context.get("method", "unknown method")
|
249
|
+
operation_context_str = json.dumps(
|
250
|
+
context.get("operation_value", {}),
|
251
|
+
indent=None,
|
252
|
+
separators=(",", ":"),
|
253
|
+
sort_keys=True,
|
254
|
+
)
|
255
|
+
if len(operation_context_str) > 500:
|
256
|
+
operation_context_str = operation_context_str[:500] + "..."
|
257
|
+
user_prompt = f"""Generate a short, unique, and readable operationId for the OpenAPI operation at path '{path_key}' using the '{method.upper()}' method.\n- The operationId MUST be a single word in camelCase or snake_case.\n- It MUST NOT exceed 30 characters.\n- It should be descriptive of the action and resource, e.g., 'getUser', 'createOrder', 'listInvoices', 'deleteUserById'.\n- Do NOT include spaces or special characters.\n- Respond ONLY with the operationId string, with NO explanation, NO notes, NO formatting, NO markdown, and NO extra text.\nContext (operation details): {operation_context_str}"""
|
258
|
+
fallback_text = f"[LLM could not generate operationId for {method.upper()} {path_key}]"
|
259
|
+
logger.info("\n--- LLM OperationId Generation Prompt ---")
|
260
|
+
logger.info(f"System prompt:\n{system_prompt}")
|
261
|
+
logger.info(f"User prompt:\n{user_prompt}")
|
262
|
+
logger.info("--- End LLM OperationId Prompt ---\n")
|
263
|
+
|
239
264
|
else:
|
240
265
|
logger.error(f"Invalid description_type '{description_type}' passed to generate_description_llm.")
|
241
266
|
return "[Invalid description type specified]"
|
@@ -244,6 +269,10 @@ def generate_description_llm(
|
|
244
269
|
logger.error(f"User prompt was not generated for description_type '{description_type}'.")
|
245
270
|
return fallback_text
|
246
271
|
|
272
|
+
# If user_prompt_override is provided in context, use it
|
273
|
+
if context and "user_prompt_override" in context:
|
274
|
+
user_prompt = context["user_prompt_override"]
|
275
|
+
|
247
276
|
messages = [
|
248
277
|
{"role": "system", "content": system_prompt},
|
249
278
|
{"role": "user", "content": user_prompt},
|
@@ -716,6 +745,7 @@ def process_parameter(
|
|
716
745
|
|
717
746
|
simplified_context = simplify_parameter_context(parameter)
|
718
747
|
|
748
|
+
current_description = parameter.get("description", "")
|
719
749
|
generated_description = generate_description_llm(
|
720
750
|
description_type="parameter",
|
721
751
|
model=llm_model,
|
@@ -725,6 +755,7 @@ def process_parameter(
|
|
725
755
|
"param_name": param_name,
|
726
756
|
"param_in": param_in,
|
727
757
|
"parameter_details": simplified_context,
|
758
|
+
"current_description": current_description,
|
728
759
|
},
|
729
760
|
)
|
730
761
|
parameter["description"] = generated_description
|
@@ -764,6 +795,8 @@ def process_operation(
|
|
764
795
|
method: str,
|
765
796
|
llm_model: str,
|
766
797
|
enhance_all: bool, # New flag
|
798
|
+
summaries_only: bool = False,
|
799
|
+
operation_ids_only: bool = False,
|
767
800
|
):
|
768
801
|
operation_location_base = f"paths.{path_key}.{method.lower()}"
|
769
802
|
|
@@ -775,6 +808,26 @@ def process_operation(
|
|
775
808
|
logger.debug(f"Skipping extension operation '{operation_location_base}'.")
|
776
809
|
return
|
777
810
|
|
811
|
+
# --- Ensure operationId is present, using LLM if missing, but only if not summaries_only ---
|
812
|
+
if (operation_ids_only or not summaries_only) and (
|
813
|
+
"operationId" not in operation_value or not operation_value["operationId"]
|
814
|
+
):
|
815
|
+
simplified_context = simplify_operation_context(operation_value)
|
816
|
+
generated_operation_id = generate_description_llm(
|
817
|
+
description_type="operation_id",
|
818
|
+
model=llm_model,
|
819
|
+
context={
|
820
|
+
"path_key": path_key,
|
821
|
+
"method": method,
|
822
|
+
"operation_value": simplified_context,
|
823
|
+
},
|
824
|
+
)
|
825
|
+
operation_value["operationId"] = sanitize_operation_id(generated_operation_id)
|
826
|
+
logger.info(f"Added operationId '{operation_value['operationId']}' to '{operation_location_base}'.")
|
827
|
+
|
828
|
+
if operation_ids_only:
|
829
|
+
return
|
830
|
+
|
778
831
|
# --- Process Summary ---
|
779
832
|
operation_summary = operation_value.get("summary")
|
780
833
|
|
@@ -814,24 +867,27 @@ def process_operation(
|
|
814
867
|
)
|
815
868
|
|
816
869
|
# --- Process Parameters ---
|
817
|
-
|
818
|
-
|
819
|
-
|
820
|
-
|
821
|
-
|
822
|
-
|
823
|
-
|
824
|
-
|
825
|
-
|
826
|
-
|
870
|
+
if not summaries_only:
|
871
|
+
parameters = operation_value.get("parameters")
|
872
|
+
if isinstance(parameters, list):
|
873
|
+
for _i, parameter in enumerate(parameters):
|
874
|
+
process_parameter(
|
875
|
+
parameter,
|
876
|
+
operation_location_base,
|
877
|
+
path_key,
|
878
|
+
method,
|
879
|
+
llm_model,
|
880
|
+
enhance_all,
|
881
|
+
)
|
882
|
+
elif parameters is not None:
|
883
|
+
logger.warning(
|
884
|
+
f"'parameters' field for operation '{operation_location_base}' is not a list. Skipping parameter processing."
|
827
885
|
)
|
828
|
-
elif parameters is not None:
|
829
|
-
logger.warning(
|
830
|
-
f"'parameters' field for operation '{operation_location_base}' is not a list. Skipping parameter processing."
|
831
|
-
)
|
832
886
|
|
833
887
|
|
834
|
-
def process_paths(
|
888
|
+
def process_paths(
|
889
|
+
paths: dict, llm_model: str, enhance_all: bool, summaries_only: bool = False, operation_ids_only: bool = False
|
890
|
+
):
|
835
891
|
if not isinstance(paths, dict):
|
836
892
|
logger.warning("'paths' field is not a dictionary. Skipping path processing.")
|
837
893
|
return
|
@@ -853,7 +909,9 @@ def process_paths(paths: dict, llm_model: str, enhance_all: bool): # New flag
|
|
853
909
|
"patch",
|
854
910
|
"trace",
|
855
911
|
]:
|
856
|
-
process_operation(
|
912
|
+
process_operation(
|
913
|
+
operation_value, path_key, method, llm_model, enhance_all, summaries_only, operation_ids_only
|
914
|
+
)
|
857
915
|
elif method.lower().startswith("x-"):
|
858
916
|
logger.debug(f"Skipping processing of method extension '{method.lower()}' in path '{path_key}'.")
|
859
917
|
continue
|
@@ -916,18 +974,89 @@ def process_info_section(schema_data: dict, llm_model: str, enhance_all: bool):
|
|
916
974
|
)
|
917
975
|
|
918
976
|
|
919
|
-
def
|
977
|
+
def find_duplicate_operation_ids(schema_data: dict) -> dict:
|
978
|
+
"""Returns a dict mapping duplicate operationIds to a list of (path, method) tuples where they occur."""
|
979
|
+
operation_id_map = {}
|
980
|
+
paths = schema_data.get("paths", {})
|
981
|
+
for path_key, path_value in paths.items():
|
982
|
+
if not isinstance(path_value, dict):
|
983
|
+
continue
|
984
|
+
for method, operation_value in path_value.items():
|
985
|
+
if method.lower() not in ["get", "put", "post", "delete", "options", "head", "patch", "trace"]:
|
986
|
+
continue
|
987
|
+
if not isinstance(operation_value, dict):
|
988
|
+
continue
|
989
|
+
op_id = operation_value.get("operationId")
|
990
|
+
if op_id:
|
991
|
+
operation_id_map.setdefault(op_id, []).append((path_key, method))
|
992
|
+
# Only keep duplicates
|
993
|
+
return {k: v for k, v in operation_id_map.items() if len(v) > 1}
|
994
|
+
|
995
|
+
|
996
|
+
def regenerate_duplicate_operation_ids(schema_data: dict, llm_model: str):
|
997
|
+
"""For each duplicate operationId, re-run the LLM to generate a new one, providing used operationIds to avoid."""
|
998
|
+
used_operation_ids = set()
|
999
|
+
paths = schema_data.get("paths", {})
|
1000
|
+
# First, collect all unique operationIds
|
1001
|
+
for _path_key, path_value in paths.items():
|
1002
|
+
if not isinstance(path_value, dict):
|
1003
|
+
continue
|
1004
|
+
for method, operation_value in path_value.items():
|
1005
|
+
if method.lower() not in ["get", "put", "post", "delete", "options", "head", "patch", "trace"]:
|
1006
|
+
continue
|
1007
|
+
if not isinstance(operation_value, dict):
|
1008
|
+
continue
|
1009
|
+
op_id = operation_value.get("operationId")
|
1010
|
+
if op_id:
|
1011
|
+
used_operation_ids.add(op_id)
|
1012
|
+
# Now, find and fix duplicates
|
1013
|
+
duplicates = find_duplicate_operation_ids(schema_data)
|
1014
|
+
for _op_id, occurrences in duplicates.items():
|
1015
|
+
# Keep the first occurrence, fix the rest
|
1016
|
+
for _path_key, method in occurrences[1:]:
|
1017
|
+
operation_value = paths[_path_key][method]
|
1018
|
+
simplified_context = simplify_operation_context(operation_value)
|
1019
|
+
# Prompt LLM with used_operation_ids to avoid
|
1020
|
+
avoid_list = list(used_operation_ids)
|
1021
|
+
user_prompt = f"Generate a short, unique, and readable operationId for the OpenAPI operation at path '{_path_key}' using the '{method.upper()}' method.\n- The operationId MUST be a single word in camelCase or snake_case.\n- It MUST NOT exceed 30 characters.\n- It should be descriptive of the action and resource, e.g., 'getUser', 'createOrder', 'listInvoices', 'deleteUserById'.\n- Do NOT include spaces or special characters.\n- Respond ONLY with the operationId string, with NO explanation, NO notes, NO formatting, NO markdown, and NO extra text.\n- Avoid these operationIds: {avoid_list}\nContext (operation details): {json.dumps(simplified_context, separators=(',', ':'), sort_keys=True)[:500]}"
|
1022
|
+
# Use the same LLM call as before, but override the user_prompt
|
1023
|
+
generated_operation_id = generate_description_llm(
|
1024
|
+
description_type="operation_id",
|
1025
|
+
model=llm_model,
|
1026
|
+
context={
|
1027
|
+
"path_key": _path_key,
|
1028
|
+
"method": method,
|
1029
|
+
"operation_value": simplified_context,
|
1030
|
+
"user_prompt_override": user_prompt,
|
1031
|
+
},
|
1032
|
+
)
|
1033
|
+
operation_value["operationId"] = sanitize_operation_id(generated_operation_id)
|
1034
|
+
used_operation_ids.add(operation_value["operationId"])
|
1035
|
+
logger.info(f"Regenerated duplicate operationId for {_path_key} {method}: {operation_value['operationId']}")
|
1036
|
+
|
1037
|
+
|
1038
|
+
def preprocess_schema_with_llm(
|
1039
|
+
schema_data: dict, llm_model: str, enhance_all: bool, summaries_only: bool = False, operation_ids_only: bool = False
|
1040
|
+
):
|
920
1041
|
"""
|
921
1042
|
Processes the schema to add/enhance descriptions/summaries using an LLM.
|
922
1043
|
Decides whether to generate based on the 'enhance_all' flag and existing content.
|
1044
|
+
If summaries_only is True, only operation summaries (and info.description) are enriched.
|
1045
|
+
If operation_ids_only is True, only missing operationIds are generated (never overwritten).
|
923
1046
|
Assumes basic schema structure validation (info, title) has already passed.
|
924
1047
|
"""
|
925
|
-
logger.info(
|
1048
|
+
logger.info(
|
1049
|
+
f"\n--- Starting LLM Generation (enhance_all={enhance_all}, summaries_only={summaries_only}, operation_ids_only={operation_ids_only}) ---"
|
1050
|
+
)
|
926
1051
|
|
927
|
-
|
1052
|
+
# Only process info section if not operation_ids_only
|
1053
|
+
if not operation_ids_only:
|
1054
|
+
process_info_section(schema_data, llm_model, enhance_all)
|
928
1055
|
|
929
1056
|
paths = schema_data.get("paths")
|
930
|
-
process_paths(paths, llm_model, enhance_all)
|
1057
|
+
process_paths(paths, llm_model, enhance_all, summaries_only, operation_ids_only)
|
1058
|
+
|
1059
|
+
# After process_paths, regenerate_duplicate_operation_ids(schema_data, llm_model)
|
931
1060
|
|
932
1061
|
logger.info("--- LLM Generation Complete ---")
|
933
1062
|
|
@@ -1007,8 +1136,11 @@ def run_preprocessing(
|
|
1007
1136
|
" [1] Generate [bold]only missing[/bold] descriptions/summaries [green](default)[/green]",
|
1008
1137
|
" [2] Generate/Enhance [bold]all[/bold] descriptions/summaries",
|
1009
1138
|
" [3] [bold red]Quit[/bold red] (exit without changes)",
|
1139
|
+
" [4] Generate/Enhance [bold]only operation summaries[/bold]",
|
1140
|
+
" [5] Generate [bold]only missing operationIds[/bold]",
|
1141
|
+
" [6] [bold]Enrich only parameter descriptions (LLM, ≤250 chars, run after clean-up)[/bold]",
|
1010
1142
|
]
|
1011
|
-
valid_choices = ["1", "2", "3"]
|
1143
|
+
valid_choices = ["1", "2", "3", "4", "5", "6"]
|
1012
1144
|
default_choice = "1" # Default to filling missing
|
1013
1145
|
|
1014
1146
|
else: # total_missing_or_fallback == 0
|
@@ -1027,8 +1159,11 @@ def run_preprocessing(
|
|
1027
1159
|
prompt_options = [
|
1028
1160
|
" [2] Generate/Enhance [bold]all[/bold] descriptions/summaries",
|
1029
1161
|
" [3] [bold red]Quit[/bold red] [green](default)[/green]",
|
1162
|
+
" [4] Generate/Enhance [bold]only operation summaries[/bold]",
|
1163
|
+
" [5] Generate [bold]only missing operationIds[/bold]",
|
1164
|
+
" [6] [bold]Enrich only parameter descriptions (LLM, ≤250 chars, run after clean-up)[/bold]",
|
1030
1165
|
]
|
1031
|
-
valid_choices = ["2", "3"]
|
1166
|
+
valid_choices = ["2", "3", "4", "5", "6"]
|
1032
1167
|
default_choice = "3" # Default to quitting if nothing missing
|
1033
1168
|
|
1034
1169
|
for option_text in prompt_options:
|
@@ -1046,19 +1181,50 @@ def run_preprocessing(
|
|
1046
1181
|
raise typer.Exit(0)
|
1047
1182
|
elif choice == "1":
|
1048
1183
|
enhance_all = False
|
1184
|
+
summaries_only = False
|
1185
|
+
operation_ids_only = False
|
1049
1186
|
break # Exit prompt loop
|
1050
1187
|
elif choice == "2":
|
1051
1188
|
enhance_all = True
|
1189
|
+
summaries_only = False
|
1190
|
+
operation_ids_only = False
|
1191
|
+
break # Exit prompt loop
|
1192
|
+
elif choice == "4":
|
1193
|
+
enhance_all = True # or False, doesn't matter since we skip parameters
|
1194
|
+
summaries_only = True
|
1195
|
+
operation_ids_only = False
|
1196
|
+
break # Exit prompt loop
|
1197
|
+
elif choice == "5":
|
1198
|
+
enhance_all = False
|
1199
|
+
summaries_only = False
|
1200
|
+
operation_ids_only = True
|
1052
1201
|
break # Exit prompt loop
|
1202
|
+
elif choice == "6":
|
1203
|
+
console.print(
|
1204
|
+
"[blue]Enriching only parameter descriptions using the LLM (≤250 chars, only if current description is longer)...[/blue]"
|
1205
|
+
)
|
1206
|
+
try:
|
1207
|
+
enrich_parameter_descriptions(schema_data, model, max_length=250)
|
1208
|
+
console.print("[green]Parameter description enrichment complete.[/green]")
|
1209
|
+
except Exception as e:
|
1210
|
+
console.print(f"[red]Error during parameter description enrichment: {e}[/red]")
|
1211
|
+
import traceback
|
1212
|
+
|
1213
|
+
traceback.print_exc(file=sys.stderr)
|
1214
|
+
raise typer.Exit(1) from e
|
1215
|
+
enhance_all = summaries_only = operation_ids_only = False
|
1216
|
+
break
|
1053
1217
|
|
1054
1218
|
perform_generation = False
|
1055
|
-
if enhance_all or choice == "1" and total_missing_or_fallback > 0:
|
1219
|
+
if operation_ids_only or summaries_only or enhance_all or (choice == "1" and total_missing_or_fallback > 0):
|
1056
1220
|
perform_generation = True
|
1057
1221
|
|
1058
1222
|
if perform_generation:
|
1059
|
-
console.print(
|
1223
|
+
console.print(
|
1224
|
+
f"[blue]Starting LLM generation with Enhance All: {enhance_all}, Summaries Only: {summaries_only}, OperationIds Only: {operation_ids_only}[/blue]"
|
1225
|
+
)
|
1060
1226
|
try:
|
1061
|
-
preprocess_schema_with_llm(schema_data, model, enhance_all)
|
1227
|
+
preprocess_schema_with_llm(schema_data, model, enhance_all, summaries_only, operation_ids_only)
|
1062
1228
|
console.print("[green]LLM generation complete.[/green]")
|
1063
1229
|
except Exception as e:
|
1064
1230
|
console.print(f"[red]Error during LLM generation: {e}[/red]")
|
@@ -1091,3 +1257,80 @@ def run_preprocessing(
|
|
1091
1257
|
console.print("\n[bold green]--- Schema Processing and Saving Complete ---[/bold green]")
|
1092
1258
|
console.print(f"Processed schema saved to: [blue]{output_path}[/blue]")
|
1093
1259
|
console.print("[bold blue]Preprocessor finished successfully.[/bold blue]")
|
1260
|
+
|
1261
|
+
|
1262
|
+
def enrich_parameter_descriptions(schema_data: dict, llm_model: str, max_length: int = 250):
|
1263
|
+
"""
|
1264
|
+
Enriches parameter descriptions using the LLM, but ONLY if the current description is longer than max_length chars.
|
1265
|
+
Only processes parameter descriptions, does not touch examples or other fields.
|
1266
|
+
"""
|
1267
|
+
MAX_ATTEMPTS = 3
|
1268
|
+
paths = schema_data.get("paths")
|
1269
|
+
if not isinstance(paths, dict):
|
1270
|
+
return
|
1271
|
+
for path_key, path_value in paths.items():
|
1272
|
+
if not isinstance(path_value, dict):
|
1273
|
+
continue
|
1274
|
+
for method, operation_value in path_value.items():
|
1275
|
+
if not (
|
1276
|
+
isinstance(operation_value, dict)
|
1277
|
+
and method.lower() in ["get", "put", "post", "delete", "options", "head", "patch", "trace"]
|
1278
|
+
):
|
1279
|
+
continue
|
1280
|
+
parameters = operation_value.get("parameters")
|
1281
|
+
if isinstance(parameters, list):
|
1282
|
+
for parameter in parameters:
|
1283
|
+
if isinstance(parameter, dict):
|
1284
|
+
param_name = parameter.get("name")
|
1285
|
+
param_in = parameter.get("in")
|
1286
|
+
# Only enrich if name and in are present
|
1287
|
+
if not (
|
1288
|
+
isinstance(param_name, str)
|
1289
|
+
and param_name.strip()
|
1290
|
+
and isinstance(param_in, str)
|
1291
|
+
and param_in.strip()
|
1292
|
+
):
|
1293
|
+
continue
|
1294
|
+
current_description = parameter.get("description", "")
|
1295
|
+
if not isinstance(current_description, str) or len(current_description) <= max_length:
|
1296
|
+
continue
|
1297
|
+
logger.info(
|
1298
|
+
f"Enriching parameter description >{max_length} chars: path='{path_key}', method='{method}', param_name='{param_name}', param_in='{param_in}', length={len(current_description)}"
|
1299
|
+
)
|
1300
|
+
simplified_context = simplify_parameter_context(parameter)
|
1301
|
+
attempt = 0
|
1302
|
+
generated_description = current_description
|
1303
|
+
while attempt < MAX_ATTEMPTS:
|
1304
|
+
generated_description = generate_description_llm(
|
1305
|
+
description_type="parameter",
|
1306
|
+
model=llm_model,
|
1307
|
+
context={
|
1308
|
+
"path_key": path_key,
|
1309
|
+
"method": method,
|
1310
|
+
"param_name": param_name,
|
1311
|
+
"param_in": param_in,
|
1312
|
+
"parameter_details": simplified_context,
|
1313
|
+
"current_description": generated_description,
|
1314
|
+
},
|
1315
|
+
)
|
1316
|
+
if isinstance(generated_description, str) and len(generated_description) <= max_length:
|
1317
|
+
break
|
1318
|
+
attempt += 1
|
1319
|
+
# If still too long, truncate
|
1320
|
+
if isinstance(generated_description, str) and len(generated_description) > max_length:
|
1321
|
+
generated_description = generated_description[:max_length].rstrip() + "..."
|
1322
|
+
parameter["description"] = generated_description
|
1323
|
+
|
1324
|
+
|
1325
|
+
def sanitize_operation_id(llm_response: str) -> str:
|
1326
|
+
"""Extracts the first valid operationId from the LLM response."""
|
1327
|
+
for line in llm_response.splitlines():
|
1328
|
+
line = line.strip()
|
1329
|
+
# Skip empty lines and markdown/comments
|
1330
|
+
if not line or line.startswith("#") or line.startswith("*") or line.startswith("**"):
|
1331
|
+
continue
|
1332
|
+
# Only allow valid operationId patterns (alphanumeric, camelCase, snake_case, max 30 chars)
|
1333
|
+
if re.match(r"^[a-zA-Z_][a-zA-Z0-9_]{0,29}$", line):
|
1334
|
+
return line
|
1335
|
+
# Fallback: return the first word
|
1336
|
+
return llm_response.strip().split()[0] if llm_response.strip() else ""
|
@@ -0,0 +1,31 @@
|
|
1
|
+
from loguru import logger
|
2
|
+
|
3
|
+
from universal_mcp.tools.tools import Tool
|
4
|
+
|
5
|
+
|
6
|
+
def check_application_instance(app_instance, app_name):
|
7
|
+
assert app_instance is not None, f"Application object is None for {app_name}"
|
8
|
+
assert (
|
9
|
+
app_instance.name == app_name
|
10
|
+
), f"Application instance name '{app_instance.name}' does not match expected name '{app_name}'"
|
11
|
+
|
12
|
+
tools = app_instance.list_tools()
|
13
|
+
logger.info(f"Tools for {app_name}: {len(tools)}")
|
14
|
+
assert len(tools) > 0, f"No tools found for {app_name}"
|
15
|
+
|
16
|
+
tools = [Tool.from_function(tool) for tool in tools]
|
17
|
+
seen_names = set()
|
18
|
+
important_tools = []
|
19
|
+
|
20
|
+
for tool in tools:
|
21
|
+
assert tool.name is not None, f"Tool name is None for a tool in {app_name}"
|
22
|
+
assert (
|
23
|
+
0 < len(tool.name) <= 48
|
24
|
+
), f"Tool name '{tool.name}' for {app_name} has invalid length (must be between 1 and 47 characters)"
|
25
|
+
assert tool.description is not None, f"Tool description is None for tool '{tool.name}' in {app_name}"
|
26
|
+
# assert 0 < len(tool.description) <= 255, f"Tool description for '{tool.name}' in {app_name} has invalid length (must be between 1 and 255 characters)"
|
27
|
+
assert tool.name not in seen_names, f"Duplicate tool name: '{tool.name}' found for {app_name}"
|
28
|
+
seen_names.add(tool.name)
|
29
|
+
if "important" in tool.tags:
|
30
|
+
important_tools.append(tool.name)
|
31
|
+
assert len(important_tools) > 0, f"No important tools found for {app_name}"
|
@@ -1,11 +1,12 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: universal-mcp
|
3
|
-
Version: 0.1.
|
3
|
+
Version: 0.1.22rc4
|
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
|
License: MIT
|
7
7
|
License-File: LICENSE
|
8
8
|
Requires-Python: >=3.11
|
9
|
+
Requires-Dist: black>=25.1.0
|
9
10
|
Requires-Dist: cookiecutter>=2.6.0
|
10
11
|
Requires-Dist: gql[all]>=3.5.2
|
11
12
|
Requires-Dist: jinja2>=3.1.3
|
@@ -6,40 +6,41 @@ universal_mcp/exceptions.py,sha256=-pbeZhpNieJfnSd2-WM80pU8W8mK8VHXcSjky0BHwdk,6
|
|
6
6
|
universal_mcp/logger.py,sha256=VmH_83efpErLEDTJqz55Dp0dioTXfGvMBLZUx5smOLc,2116
|
7
7
|
universal_mcp/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
8
8
|
universal_mcp/applications/README.md,sha256=eqbizxaTxKH2O1tyIJR2yI0Db5TQxtgPd_vbpWyCa2Y,3527
|
9
|
-
universal_mcp/applications/__init__.py,sha256=
|
9
|
+
universal_mcp/applications/__init__.py,sha256=l19_sMs5766VFWU_7O2niamvvvfQOteysqylbqvjjGQ,3500
|
10
10
|
universal_mcp/applications/application.py,sha256=3cQ5BVWmC2gU4fgpM5wZ3ByTe7iGbQriNPVSWxclaiU,17744
|
11
11
|
universal_mcp/integrations/README.md,sha256=lTAPXO2nivcBe1q7JT6PRa6v9Ns_ZersQMIdw-nmwEA,996
|
12
12
|
universal_mcp/integrations/__init__.py,sha256=X8iEzs02IlXfeafp6GMm-cOkg70QdjnlTRuFo24KEfo,916
|
13
|
-
universal_mcp/integrations/integration.py,sha256=
|
13
|
+
universal_mcp/integrations/integration.py,sha256=uKucut4AKTN2M-K8Aqsm2qtchLqlQRWMU8L287X7VyQ,13043
|
14
14
|
universal_mcp/servers/README.md,sha256=ytFlgp8-LO0oogMrHkMOp8SvFTwgsKgv7XhBVZGNTbM,2284
|
15
15
|
universal_mcp/servers/__init__.py,sha256=eBZCsaZjiEv6ZlRRslPKgurQxmpHLQyiXv2fTBygHnM,532
|
16
|
-
universal_mcp/servers/server.py,sha256=
|
16
|
+
universal_mcp/servers/server.py,sha256=K7sPdCixYgJmQRxOL1icscL7-52sVsghpRX_D_uREu4,12329
|
17
17
|
universal_mcp/stores/README.md,sha256=jrPh_ow4ESH4BDGaSafilhOVaN8oQ9IFlFW-j5Z5hLA,2465
|
18
18
|
universal_mcp/stores/__init__.py,sha256=quvuwhZnpiSLuojf0NfmBx2xpaCulv3fbKtKaSCEmuM,603
|
19
19
|
universal_mcp/stores/store.py,sha256=mxnmOVlDNrr8OKhENWDtCIfK7YeCBQcGdS6I2ogRCsU,6756
|
20
20
|
universal_mcp/tools/README.md,sha256=RuxliOFqV1ZEyeBdj3m8UKfkxAsfrxXh-b6V4ZGAk8I,2468
|
21
21
|
universal_mcp/tools/__init__.py,sha256=Fatza_R0qYWmNF1WQSfUZZKQFu5qf-16JhZzdmyx3KY,333
|
22
|
-
universal_mcp/tools/adapters.py,sha256=
|
22
|
+
universal_mcp/tools/adapters.py,sha256=nMoZ9jnv1uKhfq6NmBJ5-a6uwdB_H8RqkdNLIacCRfM,2978
|
23
23
|
universal_mcp/tools/func_metadata.py,sha256=XvdXSZEzvgbH70bc-Zu0B47CD7f_rm--vblq4en3n0Q,8181
|
24
|
-
universal_mcp/tools/manager.py,sha256=
|
25
|
-
universal_mcp/tools/tools.py,sha256=
|
24
|
+
universal_mcp/tools/manager.py,sha256=ao_ovTyca8HR4uwHdL_lTWNdquxcqRx6FaLA4U1lZvQ,11242
|
25
|
+
universal_mcp/tools/tools.py,sha256=8YBTaJCM38Nhan9Al6Vlq4FtSULrKlxg1q_o8OL1_FM,3322
|
26
26
|
universal_mcp/utils/__init__.py,sha256=8wi4PGWu-SrFjNJ8U7fr2iFJ1ktqlDmSKj1xYd7KSDc,41
|
27
|
-
universal_mcp/utils/agentr.py,sha256=
|
27
|
+
universal_mcp/utils/agentr.py,sha256=Rzfou8dfS705lNrRwKLXkpNlo4zJs1oPP_2QY46l4Uo,3486
|
28
28
|
universal_mcp/utils/common.py,sha256=HEZC2Mhilb8DrGXQG2tboAIw1r4veGilGWjfnPF1lyA,888
|
29
|
-
universal_mcp/utils/docstring_parser.py,sha256=
|
29
|
+
universal_mcp/utils/docstring_parser.py,sha256=SKIfAiFHiqxqageayYFlpsexipy8tN7N4RLT6GIzfoQ,7672
|
30
30
|
universal_mcp/utils/installation.py,sha256=ItOfBFhKOh4DLz237jgAz_Fn0uOMdrKXw0n5BaUZZNs,7286
|
31
31
|
universal_mcp/utils/singleton.py,sha256=kolHnbS9yd5C7z-tzaUAD16GgI-thqJXysNi3sZM4No,733
|
32
|
+
universal_mcp/utils/testing.py,sha256=0znYkuFi8-WjOdbwrTbNC-UpMqG3EXcGOE0wxlERh_A,1464
|
32
33
|
universal_mcp/utils/openapi/__inti__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
33
34
|
universal_mcp/utils/openapi/api_generator.py,sha256=FjtvbnWuI1P8W8wXuKLCirUtsqQ4HI_TuQrhpA4SqTs,4749
|
34
|
-
universal_mcp/utils/openapi/api_splitter.py,sha256=
|
35
|
+
universal_mcp/utils/openapi/api_splitter.py,sha256=S-rT3wsJWUVhU_Tv_ibNNAlQ79SfWOcU6qaa_rFfd5o,20806
|
35
36
|
universal_mcp/utils/openapi/docgen.py,sha256=DNmwlhg_-TRrHa74epyErMTRjV2nutfCQ7seb_Rq5hE,21366
|
36
|
-
universal_mcp/utils/openapi/openapi.py,sha256=
|
37
|
-
universal_mcp/utils/openapi/preprocessor.py,sha256=
|
37
|
+
universal_mcp/utils/openapi/openapi.py,sha256=sfHJWBaC2c-9ZCjTODH7Gt-un_agixOr_ylx9Xld8N4,51139
|
38
|
+
universal_mcp/utils/openapi/preprocessor.py,sha256=PPIM3Uu8DYi3dRKdqi9thr9ufeUgkr2K08ri1BwKpoQ,60835
|
38
39
|
universal_mcp/utils/openapi/readme.py,sha256=R2Jp7DUXYNsXPDV6eFTkLiy7MXbSULUj1vHh4O_nB4c,2974
|
39
40
|
universal_mcp/utils/templates/README.md.j2,sha256=Mrm181YX-o_-WEfKs01Bi2RJy43rBiq2j6fTtbWgbTA,401
|
40
41
|
universal_mcp/utils/templates/api_client.py.j2,sha256=972Im7LNUAq3yZTfwDcgivnb-b8u6_JLKWXwoIwXXXQ,908
|
41
|
-
universal_mcp-0.1.
|
42
|
-
universal_mcp-0.1.
|
43
|
-
universal_mcp-0.1.
|
44
|
-
universal_mcp-0.1.
|
45
|
-
universal_mcp-0.1.
|
42
|
+
universal_mcp-0.1.22rc4.dist-info/METADATA,sha256=3poFZB1sv1psL8m_KwV8FjNCWsA1tiGzB_X4QdYBaPY,12154
|
43
|
+
universal_mcp-0.1.22rc4.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
44
|
+
universal_mcp-0.1.22rc4.dist-info/entry_points.txt,sha256=QlBrVKmA2jIM0q-C-3TQMNJTTWOsOFQvgedBq2rZTS8,56
|
45
|
+
universal_mcp-0.1.22rc4.dist-info/licenses/LICENSE,sha256=NweDZVPslBAZFzlgByF158b85GR0f5_tLQgq1NS48To,1063
|
46
|
+
universal_mcp-0.1.22rc4.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|