universal-mcp-agents 0.1.21__py3-none-any.whl → 0.1.22__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.

Potentially problematic release.


This version of universal-mcp-agents might be problematic. Click here for more details.

@@ -16,10 +16,11 @@ from universal_mcp.agents.codeact0.llm_tool import smart_print
16
16
  from universal_mcp.agents.codeact0.prompts import (
17
17
  PLAYBOOK_GENERATING_PROMPT,
18
18
  PLAYBOOK_PLANNING_PROMPT,
19
+ PLAYBOOK_META_PROMPT,
19
20
  create_default_prompt,
20
21
  )
21
22
  from universal_mcp.agents.codeact0.sandbox import eval_unsafe, execute_ipython_cell, handle_execute_ipython_cell
22
- from universal_mcp.agents.codeact0.state import CodeActState, PlaybookCode, PlaybookPlan
23
+ from universal_mcp.agents.codeact0.state import CodeActState, PlaybookCode, PlaybookPlan, PlaybookMeta
23
24
  from universal_mcp.agents.codeact0.tools import (
24
25
  create_meta_tools,
25
26
  enter_playbook_mode,
@@ -28,6 +29,7 @@ from universal_mcp.agents.codeact0.tools import (
28
29
  from universal_mcp.agents.codeact0.utils import add_tools
29
30
  from universal_mcp.agents.llm import load_chat_model
30
31
  from universal_mcp.agents.utils import convert_tool_ids_to_dict, filter_retry_on, get_message_text
32
+ from universal_mcp.agents.codeact0.utils import get_connected_apps_string
31
33
 
32
34
 
33
35
  class CodeActPlaybookAgent(BaseAgent):
@@ -60,10 +62,10 @@ class CodeActPlaybookAgent(BaseAgent):
60
62
  self.sandbox_timeout = sandbox_timeout
61
63
  self.default_tools = {
62
64
  "llm": ["generate_text", "classify_data", "extract_data", "call_llm"],
63
- "markitdown": ["convert_to_markdown"],
64
65
  }
65
66
  add_tools(self.tools_config, self.default_tools)
66
67
 
68
+
67
69
  async def _build_graph(self):
68
70
  meta_tools = create_meta_tools(self.registry)
69
71
  additional_tools = [smart_print, meta_tools["web_search"]]
@@ -151,7 +153,7 @@ class CodeActPlaybookAgent(BaseAgent):
151
153
  else:
152
154
  raise Exception(
153
155
  f"Unexpected tool call: {tool_call['name']}. "
154
- "tool calls must be one of 'enter_playbook_mode', 'execute_ipython_cell', 'load_functions', or 'search_functions'"
156
+ "tool calls must be one of 'enter_playbook_mode', 'execute_ipython_cell', 'load_functions', or 'search_functions'. For using functions, call them in code using 'execute_ipython_cell'."
155
157
  )
156
158
  except Exception as e:
157
159
  tool_result = str(e)
@@ -167,7 +169,7 @@ class CodeActPlaybookAgent(BaseAgent):
167
169
  self.tools_config.extend(new_tool_ids)
168
170
  self.exported_tools = await self.registry.export_tools(new_tool_ids, ToolFormat.LANGCHAIN)
169
171
  self.final_instructions, self.tools_context = create_default_prompt(
170
- self.exported_tools, self.additional_tools, self.instructions, playbook=self.playbook
172
+ self.exported_tools, self.additional_tools, self.instructions, await get_connected_apps_string(self.registry)
171
173
  )
172
174
  if ask_user:
173
175
  tool_messages.append(AIMessage(content=ai_msg))
@@ -224,7 +226,23 @@ class CodeActPlaybookAgent(BaseAgent):
224
226
 
225
227
  t = user_text.lower()
226
228
  if t == "yes, this is great":
227
- return Command(goto="playbook", update={"playbook_mode": "generating"})
229
+ # Generate playbook metadata (name and description) before moving to generation
230
+ meta_id = str(uuid.uuid4())
231
+ writer({
232
+ "type": "custom",
233
+ id: meta_id,
234
+ "name": "metadata",
235
+ "data": {"update": bool(self.playbook)}
236
+ })
237
+ meta_instructions = self.instructions + PLAYBOOK_META_PROMPT
238
+ messages = [{"role": "system", "content": meta_instructions}] + state["messages"]
239
+
240
+ model_with_structured_output = self.playbook_model_instance.with_structured_output(PlaybookMeta)
241
+ meta_response = model_with_structured_output.invoke(messages)
242
+ meta = cast(PlaybookMeta, meta_response)
243
+
244
+ writer({"type": "custom", id: meta_id, "name": "metadata", "data": {"name": meta.name, "description": meta.description}})
245
+ return Command(goto="playbook", update={"playbook_mode": "generating", "playbook_name": meta.name, "playbook_description": meta.description})
228
246
  if t == "i would like to modify the plan":
229
247
  prompt_ai = AIMessage(content="What would you like to change about the plan? Let me know and I'll update the plan accordingly.", additional_kwargs={"stream": "true"})
230
248
  return Command(update={"playbook_mode": "planning", "messages": [prompt_ai]})
@@ -256,6 +274,10 @@ class CodeActPlaybookAgent(BaseAgent):
256
274
  else:
257
275
  function_name = "generated_playbook"
258
276
 
277
+ # Use generated metadata if available
278
+ final_name = state.get("playbook_name") or function_name
279
+ final_description = state.get("playbook_description") or f"Generated playbook: {function_name}"
280
+
259
281
  # Save or update an Agent using the helper registry
260
282
  try:
261
283
  if not self.playbook_registry:
@@ -271,8 +293,8 @@ class CodeActPlaybookAgent(BaseAgent):
271
293
  tool_dict = convert_tool_ids_to_dict(state["selected_tool_ids"])
272
294
 
273
295
  res = self.playbook_registry.upsert_agent(
274
- name=function_name,
275
- description=f"Generated playbook: {function_name}",
296
+ name=final_name,
297
+ description=final_description,
276
298
  instructions=instructions_payload,
277
299
  tools=tool_dict,
278
300
  visibility="private",
@@ -298,7 +320,7 @@ class CodeActPlaybookAgent(BaseAgent):
298
320
  self.tools_config.extend(state.get("selected_tool_ids", []))
299
321
  self.exported_tools = await self.registry.export_tools(self.tools_config, ToolFormat.LANGCHAIN)
300
322
  self.final_instructions, self.tools_context = create_default_prompt(
301
- self.exported_tools, self.additional_tools, self.instructions, playbook=self.playbook
323
+ self.exported_tools, self.additional_tools, self.instructions, await get_connected_apps_string(self.registry)
302
324
  )
303
325
  if state.get("playbook_mode") in ["planning", "confirming", "generating"]:
304
326
  return "playbook"
@@ -3,6 +3,7 @@ import re
3
3
  from collections.abc import Sequence
4
4
 
5
5
  from langchain_core.tools import StructuredTool
6
+ from universal_mcp.agents.codeact0.utils import schema_to_signature
6
7
 
7
8
  uneditable_prompt = """
8
9
  You are **Ruzo**, an AI Assistant created by AgentR — a creative, straight-forward, and direct principal software engineer with access to tools.
@@ -97,6 +98,26 @@ The parameters of the function should be the same as the final confirmed playboo
97
98
  """
98
99
 
99
100
 
101
+ PLAYBOOK_META_PROMPT = """
102
+ You are preparing metadata for a reusable playbook based on the confirmed step-by-step plan.
103
+
104
+ TASK: Create a concise, human-friendly name and a short description for the playbook.
105
+
106
+ INPUTS:
107
+ - Conversation context and plan steps will be provided in prior messages
108
+
109
+ REQUIREMENTS:
110
+ 1. Name: 3-6 words, Title Case, no punctuation except hyphens if needed
111
+ 2. Description: Single sentence, <= 140 characters, clearly states what the playbook does
112
+
113
+ OUTPUT: Return ONLY a JSON object with exactly these keys:
114
+ {
115
+ "name": "...",
116
+ "description": "..."
117
+ }
118
+ """
119
+
120
+
100
121
  def make_safe_function_name(name: str) -> str:
101
122
  """Convert a tool name to a valid Python function name."""
102
123
  # Replace non-alphanumeric characters with underscores
@@ -114,14 +135,31 @@ def create_default_prompt(
114
135
  tools: Sequence[StructuredTool],
115
136
  additional_tools: Sequence[StructuredTool],
116
137
  base_prompt: str | None = None,
138
+ apps_string: str | None = None,
117
139
  playbook: object | None = None,
118
140
  ):
119
- system_prompt = uneditable_prompt.strip() + (
120
- "\n\nIn addition to the Python Standard Library, you can use the following external functions:\n"
121
- )
141
+ system_prompt = uneditable_prompt.strip()
142
+ if apps_string:
143
+ system_prompt += f"\n\n**Connected external applications (These apps have been logged into by the user):**\n{apps_string}\n\n Use `search_functions` to search for functions you can perform using the above. You can also discover more applications using the `search_functions` tool to find additional tools and integrations, if required.\n"
144
+ system_prompt += "\n\nIn addition to the Python Standard Library, you can use the following external functions:\n"
145
+
122
146
  tools_context = {}
123
147
 
124
- for tool in tools + additional_tools:
148
+ for tool in tools:
149
+ if hasattr(tool, "func") and tool.func is not None:
150
+ tool_callable = tool.func
151
+ is_async = False
152
+ elif hasattr(tool, "coroutine") and tool.coroutine is not None:
153
+ tool_callable = tool.coroutine
154
+ is_async = True
155
+ system_prompt += f'''{"async " if is_async else ""}{schema_to_signature(tool.args, tool.name)}:
156
+ """{tool.description}"""
157
+ ...
158
+ '''
159
+ safe_name = make_safe_function_name(tool.name)
160
+ tools_context[safe_name] = tool_callable
161
+
162
+ for tool in additional_tools:
125
163
  if hasattr(tool, "func") and tool.func is not None:
126
164
  tool_callable = tool.func
127
165
  is_async = False
@@ -131,7 +169,7 @@ def create_default_prompt(
131
169
  system_prompt += f'''{"async " if is_async else ""}def {tool.name} {str(inspect.signature(tool_callable))}:
132
170
  """{tool.description}"""
133
171
  ...
134
- '''
172
+ '''
135
173
  safe_name = make_safe_function_name(tool.name)
136
174
  tools_context[safe_name] = tool_callable
137
175
 
@@ -12,6 +12,11 @@ class PlaybookCode(BaseModel):
12
12
  code: str = Field(description="The Python code for the playbook.")
13
13
 
14
14
 
15
+ class PlaybookMeta(BaseModel):
16
+ name: str = Field(description="Concise, title-cased playbook name (3-6 words).")
17
+ description: str = Field(description="Short, one-sentence description (<= 140 chars).")
18
+
19
+
15
20
  def _enqueue(left: list, right: list) -> list:
16
21
  """Treat left as a FIFO queue, append new items from right (preserve order),
17
22
  keep items unique, and cap total size to 20 (drop oldest items)."""
@@ -47,3 +52,7 @@ class CodeActState(AgentState):
47
52
  """Queue for tools exported from registry"""
48
53
  plan: list[str] | None
49
54
  """Plan for the playbook agent."""
55
+ playbook_name: str | None
56
+ """Generated playbook name after confirmation."""
57
+ playbook_description: str | None
58
+ """Generated short description after confirmation."""
@@ -70,9 +70,10 @@ def create_meta_tools(tool_registry: AgentrRegistry) -> dict[str, Any]:
70
70
 
71
71
  canonical_app_id = None
72
72
  found_tools_result = []
73
+ THRESHOLD = 0.8
73
74
 
74
75
  if app_id:
75
- relevant_apps = await registry.search_apps(query=app_id, distance_threshold=0.7)
76
+ relevant_apps = await registry.search_apps(query=app_id, distance_threshold=THRESHOLD)
76
77
  if not relevant_apps:
77
78
  return {
78
79
  "found_tools": [],
@@ -104,11 +105,11 @@ def create_meta_tools(tool_registry: AgentrRegistry) -> dict[str, Any]:
104
105
  prioritized_app_id_list = [canonical_app_id]
105
106
  else:
106
107
  # 1. Perform an initial broad search for tools.
107
- initial_tool_search_tasks = [registry.search_tools(query=q, distance_threshold=0.7) for q in queries]
108
+ initial_tool_search_tasks = [registry.search_tools(query=q, distance_threshold=THRESHOLD) for q in queries]
108
109
  initial_tool_results = await asyncio.gather(*initial_tool_search_tasks)
109
110
 
110
111
  # 2. Search for relevant apps.
111
- app_search_tasks = [registry.search_apps(query=q, distance_threshold=0.7) for q in queries]
112
+ app_search_tasks = [registry.search_apps(query=q, distance_threshold=THRESHOLD) for q in queries]
112
113
  app_search_results = await asyncio.gather(*app_search_tasks)
113
114
 
114
115
  # 3. Create a prioritized list of app IDs for the final search.
@@ -131,7 +132,7 @@ def create_meta_tools(tool_registry: AgentrRegistry) -> dict[str, Any]:
131
132
  for app_id_to_search in prioritized_app_id_list:
132
133
  for query in queries:
133
134
  final_tool_search_tasks.append(
134
- registry.search_tools(query=query, app_id=app_id_to_search, distance_threshold=0.7)
135
+ registry.search_tools(query=query, app_id=app_id_to_search, distance_threshold=THRESHOLD)
135
136
  )
136
137
  query_results = await asyncio.gather(*final_tool_search_tasks)
137
138
 
@@ -209,29 +210,27 @@ def create_meta_tools(tool_registry: AgentrRegistry) -> dict[str, Any]:
209
210
 
210
211
  @tool
211
212
  async def web_search(query: str) -> dict:
212
- """Get an LLM answer to a question informed by Perplexity web search results.
213
- Useful when you need information from a wide range of real-time sources on the web.
214
- Do not use this when you need to access contents of a specific webpage.
213
+ """
214
+ Get an LLM answer to a question informed by Exa search results. Useful when you need information from a wide range of real-time sources on the web. Do not use this when you need to access contents of a specific webpage.
215
215
 
216
- This tool performs a Perplexity request via `perplexity__answer_with_search`, which:
217
- 1. Provides a **direct answer** for factual queries with citation(s)
216
+ This tool performs an Exa `/answer` request, which:
217
+ 1. Provides a **direct answer** for factual queries (e.g., "What is the capital of France?" → "Paris")
218
218
  2. Generates a **summary with citations** for open-ended questions
219
+ (e.g., "What is the state of AI in healthcare?" → A detailed summary with source links)
219
220
 
220
221
  Args:
221
222
  query (str): The question or topic to answer.
222
-
223
223
  Returns:
224
- dict: A structured response containing:
225
- - answer (str): Generated answer with markdown formatting and citation numbers [1][2]
226
- - citations (list[str]): List of source URLs corresponding to citation numbers
224
+ dict: A structured response containing only:
225
+ - answer (str): Generated answer
226
+ - citations (list[dict]): List of cited sources
227
227
  """
228
228
  await tool_registry.export_tools(["exa__answer"], ToolFormat.LANGCHAIN)
229
229
  response = await tool_registry.call_tool("exa__answer", {"query": query, "text": True})
230
- logger.info(f"Web search response: {response}")
231
230
 
232
231
  # Extract only desired fields
233
232
  return {
234
- "answer": response.get("content"),
233
+ "answer": response.get("answer"),
235
234
  "citations": response.get("citations", []),
236
235
  }
237
236
 
@@ -279,7 +278,7 @@ async def get_valid_tools(tool_ids: list[str], registry: AgentrRegistry) -> tupl
279
278
  continue
280
279
  if app not in connected_apps and app not in unconnected:
281
280
  unconnected.add(app)
282
- text = registry.authorise_app(app_id=app)
281
+ text = await registry.authorise_app(app_id=app)
283
282
  start = text.find(":") + 1
284
283
  end = text.find(". R", start)
285
284
  url = text[start:end].strip()
@@ -333,35 +333,48 @@ def inject_context(
333
333
  return namespace
334
334
 
335
335
 
336
- def schema_to_signature(schema: dict, func_name="my_function") -> str:
336
+ def schema_to_signature(schema: dict, func_name: str = "my_function") -> str:
337
+ """
338
+ Convert a JSON schema into a Python-style function signature string.
339
+ Handles fields with `type`, `anyOf`, defaults, and missing metadata safely.
340
+ """
337
341
  type_map = {
338
342
  "integer": "int",
339
343
  "string": "str",
340
344
  "boolean": "bool",
341
345
  "null": "None",
346
+ "number": "float",
347
+ "array": "list",
348
+ "object": "dict",
342
349
  }
343
350
 
344
351
  params = []
345
352
  for name, meta in schema.items():
346
- # figure out type
347
- if "type" in meta:
353
+ if not isinstance(meta, dict):
354
+ typ = "Any"
355
+ elif "type" in meta:
348
356
  typ = type_map.get(meta["type"], "Any")
349
357
  elif "anyOf" in meta:
350
- types = [type_map.get(t["type"], "Any") for t in meta["anyOf"]]
351
- typ = " | ".join(set(types))
358
+ types = []
359
+ for t in meta["anyOf"]:
360
+ if not isinstance(t, dict):
361
+ continue
362
+ t_type = t.get("type")
363
+ types.append(type_map.get(t_type, "Any") if t_type else "Any")
364
+ typ = " | ".join(sorted(set(types))) if types else "Any"
352
365
  else:
353
366
  typ = "Any"
354
367
 
355
- default = meta.get("default", None)
356
- default_repr = repr(default)
357
-
358
- params.append(f"{name}: {typ} = {default_repr}")
368
+ # Handle defaults gracefully
369
+ default = meta.get("default")
370
+ if default is None:
371
+ params.append(f"{name}: {typ}")
372
+ else:
373
+ params.append(f"{name}: {typ} = {repr(default)}")
359
374
 
360
- # join into signature
361
375
  param_str = ",\n ".join(params)
362
376
  return f"def {func_name}(\n {param_str},\n):"
363
377
 
364
-
365
378
  def smart_truncate(
366
379
  output: str, max_chars_full: int = 2000, max_lines_headtail: int = 20, summary_threshold: int = 10000
367
380
  ) -> str:
@@ -394,3 +407,27 @@ def smart_truncate(
394
407
  truncated = truncated[:summary_threshold] + "\n... [output truncated to fit context] ..."
395
408
 
396
409
  return truncated
410
+
411
+
412
+ async def get_connected_apps_string(registry) -> str:
413
+ """Get a formatted string of connected applications from the registry."""
414
+ if not registry:
415
+ return ""
416
+
417
+ try:
418
+ # Get connected apps from registry
419
+ connections = await registry.list_connected_apps()
420
+ if not connections:
421
+ return "No applications are currently connected."
422
+
423
+ # Extract app names from connections
424
+ connected_app_ids = {connection["app_id"] for connection in connections}
425
+
426
+ # Format the apps list
427
+ apps_list = []
428
+ for app_id in connected_app_ids:
429
+ apps_list.append(f"- {app_id}")
430
+
431
+ return "\n".join(apps_list)
432
+ except Exception:
433
+ return "Unable to retrieve connected applications."
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: universal-mcp-agents
3
- Version: 0.1.21
3
+ Version: 0.1.22
4
4
  Summary: Add your description here
5
5
  Project-URL: Homepage, https://github.com/universal-mcp/applications
6
6
  Project-URL: Repository, https://github.com/universal-mcp/applications
@@ -22,15 +22,15 @@ universal_mcp/agents/builder/prompts.py,sha256=8Xs6uzTUHguDRngVMLak3lkXFkk2VV_uQ
22
22
  universal_mcp/agents/builder/state.py,sha256=7DeWllxfN-yD6cd9wJ3KIgjO8TctkJvVjAbZT8W_zqk,922
23
23
  universal_mcp/agents/codeact0/__init__.py,sha256=8-fvUo1Sm6dURGI-lW-X3Kd78LqySYbb5NMkNJ4NDwg,76
24
24
  universal_mcp/agents/codeact0/__main__.py,sha256=_7qSz97YnRgYJTESkALS5_eBIGHiMjA5rhr3IAeBvVo,896
25
- universal_mcp/agents/codeact0/agent.py,sha256=KvIoZh2J2JQONfGf80LBuXDdQnnYja4ybZ4DluCKxPM,15192
25
+ universal_mcp/agents/codeact0/agent.py,sha256=Cxdh9eJvrIfUISdFQKXnYrGc6PePrIGusSvWiALmAJo,16634
26
26
  universal_mcp/agents/codeact0/config.py,sha256=H-1woj_nhSDwf15F63WYn723y4qlRefXzGxuH81uYF0,2215
27
27
  universal_mcp/agents/codeact0/langgraph_agent.py,sha256=8nz2wq-LexImx-l1y9_f81fK72IQetnCeljwgnduNGY,420
28
28
  universal_mcp/agents/codeact0/llm_tool.py,sha256=-pAz04OrbZ_dJ2ueysT1qZd02DrbLY4EbU0tiuF_UNU,798
29
- universal_mcp/agents/codeact0/prompts.py,sha256=a5ja7KwSG1SmbCkoZVdyFOI5dz2Ul-OWG-JcakBySDM,9635
29
+ universal_mcp/agents/codeact0/prompts.py,sha256=Gk6WTx2X7IOlbC3wYtGwKWDoeUtgpkMeiyfhj6LEq2I,11221
30
30
  universal_mcp/agents/codeact0/sandbox.py,sha256=Xw4tbUV_6haYIZZvteJi6lIYsW6ni_3DCRCOkslTKgM,4459
31
- universal_mcp/agents/codeact0/state.py,sha256=Z9fUmBTw-29MZTq2GsYYyyjLPyrtE5GDnhX4gSqHhcA,1555
32
- universal_mcp/agents/codeact0/tools.py,sha256=qGhbJl74xflJBNfOCiy-Ts5zKIxXjE1JVyqvtVGHZNY,13363
33
- universal_mcp/agents/codeact0/utils.py,sha256=Rustf3MFoFbasdnwBh9YvNrXqasgANkBAWQn9AjqBO4,16240
31
+ universal_mcp/agents/codeact0/state.py,sha256=co3BZBuMIt1FP2qzgsbsLkyKbddCG1ieKyAw9TAskSU,1944
32
+ universal_mcp/agents/codeact0/tools.py,sha256=WRJKNIf6_cP_enElboEyVd-aHF-kk6aq8Bg0biZijR4,13363
33
+ universal_mcp/agents/codeact0/utils.py,sha256=PgisAxmqYIzimc4lFA1nV-R7gxkICIrtO1q0FQZ-UoY,17580
34
34
  universal_mcp/agents/shared/__main__.py,sha256=XxH5qGDpgFWfq7fwQfgKULXGiUgeTp_YKfcxftuVZq8,1452
35
35
  universal_mcp/agents/shared/prompts.py,sha256=yjP3zbbuKi87qCj21qwTTicz8TqtkKgnyGSeEjMu3ho,3761
36
36
  universal_mcp/agents/shared/tool_node.py,sha256=DC9F-Ri28Pam0u3sXWNODVgmj9PtAEUb5qP1qOoGgfs,9169
@@ -39,6 +39,6 @@ universal_mcp/applications/filesystem/app.py,sha256=0TRjjm8YnslVRSmfkXI7qQOAlqWl
39
39
  universal_mcp/applications/llm/__init__.py,sha256=_XGRxN3O1--ZS5joAsPf8IlI9Qa6negsJrwJ5VJXno0,46
40
40
  universal_mcp/applications/llm/app.py,sha256=g9mK-luOLUshZzBGyQZMOHBeCSXmh2kCKir40YnsGUo,12727
41
41
  universal_mcp/applications/ui/app.py,sha256=c7OkZsO2fRtndgAzAQbKu-1xXRuRp9Kjgml57YD2NR4,9459
42
- universal_mcp_agents-0.1.21.dist-info/METADATA,sha256=2Nx9bEG_IEb5wHTV-8vXpQEUTNsl0bbQkPR6Ii4R1V0,878
43
- universal_mcp_agents-0.1.21.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
44
- universal_mcp_agents-0.1.21.dist-info/RECORD,,
42
+ universal_mcp_agents-0.1.22.dist-info/METADATA,sha256=6f34bvvybyI6et8rvKIu-4BtFJHGmt0YZmaZutJBXuw,878
43
+ universal_mcp_agents-0.1.22.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
44
+ universal_mcp_agents-0.1.22.dist-info/RECORD,,