universal-mcp 0.1.13rc2__tar.gz → 0.1.13rc7__tar.gz

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 (58) hide show
  1. {universal_mcp-0.1.13rc2 → universal_mcp-0.1.13rc7}/PKG-INFO +40 -1
  2. {universal_mcp-0.1.13rc2 → universal_mcp-0.1.13rc7}/pyproject.toml +41 -1
  3. {universal_mcp-0.1.13rc2 → universal_mcp-0.1.13rc7}/src/playground/__main__.py +3 -1
  4. {universal_mcp-0.1.13rc2 → universal_mcp-0.1.13rc7}/src/playground/agents/react.py +1 -1
  5. {universal_mcp-0.1.13rc2 → universal_mcp-0.1.13rc7}/src/playground/client.py +40 -41
  6. {universal_mcp-0.1.13rc2 → universal_mcp-0.1.13rc7}/src/playground/streamlit.py +21 -11
  7. {universal_mcp-0.1.13rc2 → universal_mcp-0.1.13rc7}/src/tests/test_api_generator.py +3 -4
  8. {universal_mcp-0.1.13rc2 → universal_mcp-0.1.13rc7}/src/tests/test_api_integration.py +2 -2
  9. {universal_mcp-0.1.13rc2 → universal_mcp-0.1.13rc7}/src/tests/test_applications.py +1 -1
  10. {universal_mcp-0.1.13rc2 → universal_mcp-0.1.13rc7}/src/tests/test_stores.py +1 -0
  11. {universal_mcp-0.1.13rc2 → universal_mcp-0.1.13rc7}/src/universal_mcp/applications/__init__.py +20 -6
  12. {universal_mcp-0.1.13rc2 → universal_mcp-0.1.13rc7}/src/universal_mcp/cli.py +14 -16
  13. {universal_mcp-0.1.13rc2 → universal_mcp-0.1.13rc7}/src/universal_mcp/servers/server.py +0 -2
  14. {universal_mcp-0.1.13rc2 → universal_mcp-0.1.13rc7}/src/universal_mcp/stores/store.py +2 -0
  15. {universal_mcp-0.1.13rc2 → universal_mcp-0.1.13rc7}/src/universal_mcp/tools/tools.py +1 -3
  16. {universal_mcp-0.1.13rc2 → universal_mcp-0.1.13rc7}/src/universal_mcp/utils/agentr.py +8 -3
  17. {universal_mcp-0.1.13rc2 → universal_mcp-0.1.13rc7}/src/universal_mcp/utils/api_generator.py +26 -23
  18. {universal_mcp-0.1.13rc2 → universal_mcp-0.1.13rc7}/src/universal_mcp/utils/installation.py +8 -8
  19. {universal_mcp-0.1.13rc2 → universal_mcp-0.1.13rc7}/src/universal_mcp/utils/openapi.py +54 -38
  20. universal_mcp-0.1.13rc2/src/playground/memory/__init__.py +0 -14
  21. universal_mcp-0.1.13rc2/src/playground/memory/sqlite.py +0 -9
  22. {universal_mcp-0.1.13rc2 → universal_mcp-0.1.13rc7}/.gitignore +0 -0
  23. {universal_mcp-0.1.13rc2 → universal_mcp-0.1.13rc7}/README.md +0 -0
  24. {universal_mcp-0.1.13rc2 → universal_mcp-0.1.13rc7}/src/playground/README.md +0 -0
  25. {universal_mcp-0.1.13rc2 → universal_mcp-0.1.13rc7}/src/playground/__init__.py +0 -0
  26. {universal_mcp-0.1.13rc2 → universal_mcp-0.1.13rc7}/src/playground/schema.py +0 -0
  27. {universal_mcp-0.1.13rc2 → universal_mcp-0.1.13rc7}/src/playground/settings.py +0 -0
  28. {universal_mcp-0.1.13rc2 → universal_mcp-0.1.13rc7}/src/playground/utils.py +0 -0
  29. {universal_mcp-0.1.13rc2 → universal_mcp-0.1.13rc7}/src/tests/__init__.py +0 -0
  30. {universal_mcp-0.1.13rc2 → universal_mcp-0.1.13rc7}/src/tests/conftest.py +0 -0
  31. {universal_mcp-0.1.13rc2 → universal_mcp-0.1.13rc7}/src/tests/test_localserver.py +0 -0
  32. {universal_mcp-0.1.13rc2 → universal_mcp-0.1.13rc7}/src/tests/test_tool.py +0 -0
  33. {universal_mcp-0.1.13rc2 → universal_mcp-0.1.13rc7}/src/tests/test_zenquotes.py +0 -0
  34. {universal_mcp-0.1.13rc2 → universal_mcp-0.1.13rc7}/src/universal_mcp/__init__.py +0 -0
  35. {universal_mcp-0.1.13rc2 → universal_mcp-0.1.13rc7}/src/universal_mcp/analytics.py +0 -0
  36. {universal_mcp-0.1.13rc2 → universal_mcp-0.1.13rc7}/src/universal_mcp/applications/application.py +0 -0
  37. {universal_mcp-0.1.13rc2 → universal_mcp-0.1.13rc7}/src/universal_mcp/config.py +0 -0
  38. {universal_mcp-0.1.13rc2 → universal_mcp-0.1.13rc7}/src/universal_mcp/exceptions.py +0 -0
  39. {universal_mcp-0.1.13rc2 → universal_mcp-0.1.13rc7}/src/universal_mcp/integrations/README.md +0 -0
  40. {universal_mcp-0.1.13rc2 → universal_mcp-0.1.13rc7}/src/universal_mcp/integrations/__init__.py +1 -1
  41. {universal_mcp-0.1.13rc2 → universal_mcp-0.1.13rc7}/src/universal_mcp/integrations/integration.py +0 -0
  42. {universal_mcp-0.1.13rc2 → universal_mcp-0.1.13rc7}/src/universal_mcp/logger.py +0 -0
  43. {universal_mcp-0.1.13rc2 → universal_mcp-0.1.13rc7}/src/universal_mcp/py.typed +0 -0
  44. {universal_mcp-0.1.13rc2 → universal_mcp-0.1.13rc7}/src/universal_mcp/servers/README.md +0 -0
  45. {universal_mcp-0.1.13rc2 → universal_mcp-0.1.13rc7}/src/universal_mcp/servers/__init__.py +0 -0
  46. {universal_mcp-0.1.13rc2 → universal_mcp-0.1.13rc7}/src/universal_mcp/stores/README.md +0 -0
  47. {universal_mcp-0.1.13rc2 → universal_mcp-0.1.13rc7}/src/universal_mcp/stores/__init__.py +0 -0
  48. {universal_mcp-0.1.13rc2 → universal_mcp-0.1.13rc7}/src/universal_mcp/templates/README.md.j2 +0 -0
  49. {universal_mcp-0.1.13rc2 → universal_mcp-0.1.13rc7}/src/universal_mcp/templates/api_client.py.j2 +0 -0
  50. {universal_mcp-0.1.13rc2 → universal_mcp-0.1.13rc7}/src/universal_mcp/tools/README.md +0 -0
  51. {universal_mcp-0.1.13rc2 → universal_mcp-0.1.13rc7}/src/universal_mcp/tools/__init__.py +0 -0
  52. {universal_mcp-0.1.13rc2 → universal_mcp-0.1.13rc7}/src/universal_mcp/tools/adapters.py +0 -0
  53. {universal_mcp-0.1.13rc2 → universal_mcp-0.1.13rc7}/src/universal_mcp/tools/func_metadata.py +0 -0
  54. {universal_mcp-0.1.13rc2 → universal_mcp-0.1.13rc7}/src/universal_mcp/utils/__init__.py +0 -0
  55. {universal_mcp-0.1.13rc2 → universal_mcp-0.1.13rc7}/src/universal_mcp/utils/docgen.py +0 -0
  56. {universal_mcp-0.1.13rc2 → universal_mcp-0.1.13rc7}/src/universal_mcp/utils/docstring_parser.py +0 -0
  57. {universal_mcp-0.1.13rc2 → universal_mcp-0.1.13rc7}/src/universal_mcp/utils/dump_app_tools.py +0 -0
  58. {universal_mcp-0.1.13rc2 → universal_mcp-0.1.13rc7}/src/universal_mcp/utils/singleton.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: universal-mcp
3
- Version: 0.1.13rc2
3
+ Version: 0.1.13rc7
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
@@ -31,6 +31,45 @@ Requires-Dist: python-dotenv>=1.0.1; extra == 'all'
31
31
  Requires-Dist: ruff>=0.11.4; extra == 'all'
32
32
  Requires-Dist: streamlit>=1.44.1; extra == 'all'
33
33
  Requires-Dist: watchdog>=6.0.0; extra == 'all'
34
+ Provides-Extra: applications
35
+ Requires-Dist: universal-mcp-ahrefs; extra == 'applications'
36
+ Requires-Dist: universal-mcp-cal-com-v2; extra == 'applications'
37
+ Requires-Dist: universal-mcp-calendly; extra == 'applications'
38
+ Requires-Dist: universal-mcp-clickup; extra == 'applications'
39
+ Requires-Dist: universal-mcp-coda; extra == 'applications'
40
+ Requires-Dist: universal-mcp-crustdata; extra == 'applications'
41
+ Requires-Dist: universal-mcp-e2b; extra == 'applications'
42
+ Requires-Dist: universal-mcp-elevenlabs; extra == 'applications'
43
+ Requires-Dist: universal-mcp-falai; extra == 'applications'
44
+ Requires-Dist: universal-mcp-figma; extra == 'applications'
45
+ Requires-Dist: universal-mcp-firecrawl; extra == 'applications'
46
+ Requires-Dist: universal-mcp-github; extra == 'applications'
47
+ Requires-Dist: universal-mcp-gong; extra == 'applications'
48
+ Requires-Dist: universal-mcp-google-calendar; extra == 'applications'
49
+ Requires-Dist: universal-mcp-google-docs; extra == 'applications'
50
+ Requires-Dist: universal-mcp-google-drive; extra == 'applications'
51
+ Requires-Dist: universal-mcp-google-mail; extra == 'applications'
52
+ Requires-Dist: universal-mcp-google-sheet; extra == 'applications'
53
+ Requires-Dist: universal-mcp-hashnode; extra == 'applications'
54
+ Requires-Dist: universal-mcp-heygen; extra == 'applications'
55
+ Requires-Dist: universal-mcp-mailchimp; extra == 'applications'
56
+ Requires-Dist: universal-mcp-markitdown; extra == 'applications'
57
+ Requires-Dist: universal-mcp-neon; extra == 'applications'
58
+ Requires-Dist: universal-mcp-notion; extra == 'applications'
59
+ Requires-Dist: universal-mcp-perplexity; extra == 'applications'
60
+ Requires-Dist: universal-mcp-reddit; extra == 'applications'
61
+ Requires-Dist: universal-mcp-replicate; extra == 'applications'
62
+ Requires-Dist: universal-mcp-resend; extra == 'applications'
63
+ Requires-Dist: universal-mcp-retell; extra == 'applications'
64
+ Requires-Dist: universal-mcp-rocketlane; extra == 'applications'
65
+ Requires-Dist: universal-mcp-serpapi; extra == 'applications'
66
+ Requires-Dist: universal-mcp-shortcut; extra == 'applications'
67
+ Requires-Dist: universal-mcp-spotify; extra == 'applications'
68
+ Requires-Dist: universal-mcp-supabase; extra == 'applications'
69
+ Requires-Dist: universal-mcp-tavily; extra == 'applications'
70
+ Requires-Dist: universal-mcp-wrike; extra == 'applications'
71
+ Requires-Dist: universal-mcp-youtube; extra == 'applications'
72
+ Requires-Dist: universal-mcp-zenquotes; extra == 'applications'
34
73
  Provides-Extra: dev
35
74
  Requires-Dist: litellm>=1.30.7; extra == 'dev'
36
75
  Requires-Dist: pyright>=1.1.398; extra == 'dev'
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "universal-mcp"
3
- version = "0.1.13-rc2"
3
+ version = "0.1.13-rc7"
4
4
  description = "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
  readme = "README.md"
6
6
  authors = [
@@ -57,6 +57,46 @@ all = [
57
57
  "pyright>=1.1.398",
58
58
  "litellm>=1.30.7",
59
59
  ]
60
+ applications = [
61
+ "universal-mcp-ahrefs",
62
+ "universal-mcp-cal-com-v2",
63
+ "universal-mcp-calendly",
64
+ "universal-mcp-clickup",
65
+ "universal-mcp-coda",
66
+ "universal-mcp-crustdata",
67
+ "universal-mcp-e2b",
68
+ "universal-mcp-elevenlabs",
69
+ "universal-mcp-falai",
70
+ "universal-mcp-figma",
71
+ "universal-mcp-firecrawl",
72
+ "universal-mcp-github",
73
+ "universal-mcp-gong",
74
+ "universal-mcp-google-calendar",
75
+ "universal-mcp-google-docs",
76
+ "universal-mcp-google-drive",
77
+ "universal-mcp-google-mail",
78
+ "universal-mcp-google-sheet",
79
+ "universal-mcp-hashnode",
80
+ "universal-mcp-heygen",
81
+ "universal-mcp-mailchimp",
82
+ "universal-mcp-markitdown",
83
+ "universal-mcp-neon",
84
+ "universal-mcp-notion",
85
+ "universal-mcp-perplexity",
86
+ "universal-mcp-reddit",
87
+ "universal-mcp-replicate",
88
+ "universal-mcp-resend",
89
+ "universal-mcp-retell",
90
+ "universal-mcp-rocketlane",
91
+ "universal-mcp-serpapi",
92
+ "universal-mcp-shortcut",
93
+ "universal-mcp-spotify",
94
+ "universal-mcp-supabase",
95
+ "universal-mcp-tavily",
96
+ "universal-mcp-wrike",
97
+ "universal-mcp-youtube",
98
+ "universal-mcp-zenquotes",
99
+ ]
60
100
 
61
101
  [project.scripts]
62
102
  universal_mcp = "universal_mcp.cli:app"
@@ -11,7 +11,9 @@ def main():
11
11
  # Ask the user if they want to run the MCP server
12
12
  run_mcp_server = input("Do you want to run the MCP server? (y/n): ")
13
13
  if run_mcp_server == "y":
14
- mcp_process = subprocess.Popen(["universal_mcp", "run", "-c", "local_config.json"])
14
+ mcp_process = subprocess.Popen(
15
+ ["universal_mcp", "run", "-c", "local_config.json"]
16
+ )
15
17
  processes.append(mcp_process)
16
18
  time.sleep(6) # Give MCP server time to start
17
19
  logger.info("MCP server started")
@@ -12,7 +12,7 @@ async def load_tools():
12
12
  async with MultiServerMCPClient(
13
13
  {
14
14
  "agentr": {
15
- "url": "http://localhost:8000/sse",
15
+ "url": "http://localhost:8005/sse",
16
16
  "transport": "sse",
17
17
  },
18
18
  }
@@ -1,35 +1,29 @@
1
- from contextlib import asynccontextmanager
2
1
  import json
3
- import os
4
2
  from collections.abc import AsyncGenerator, Generator
3
+ from contextlib import asynccontextmanager
5
4
  from typing import Any
6
- from uuid import UUID, uuid4
5
+ from uuid import uuid4
7
6
 
8
7
  from langchain_core.messages import HumanMessage
9
8
  from langchain_core.runnables import RunnableConfig
9
+ from langgraph.types import Command
10
10
 
11
11
  from playground.agents.react import create_agent
12
- from playground.memory import initialize_database
13
12
  from playground.schema import (
14
13
  ChatHistory,
15
- ChatHistoryInput,
16
14
  ChatMessage,
17
- StreamInput,
18
- UserInput,
19
15
  )
20
16
  from playground.utils import (
21
17
  convert_message_content_to_string,
22
18
  langchain_to_chat_message,
23
19
  remove_tool_calls,
24
20
  )
25
- from langgraph.types import Command
21
+
26
22
 
27
23
  @asynccontextmanager
28
24
  async def create_agent_client():
29
- async with create_agent() as react_agent, initialize_database() as saver:
30
- await saver.setup()
25
+ async with create_agent() as react_agent:
31
26
  agent = react_agent
32
- agent.checkpointer = saver
33
27
  client = AgentClient(agent=agent)
34
28
  yield client
35
29
 
@@ -41,10 +35,7 @@ class AgentClientError(Exception):
41
35
  class AgentClient:
42
36
  """Client for interacting with the agent directly."""
43
37
 
44
- def __init__(
45
- self,
46
- agent
47
- ) -> None:
38
+ def __init__(self, agent) -> None:
48
39
  """
49
40
  Initialize the client.
50
41
 
@@ -82,7 +73,9 @@ class AgentClient:
82
73
  configurable = {"thread_id": thread_id}
83
74
  if agent_config:
84
75
  if overlap := configurable.keys() & agent_config.keys():
85
- raise AgentClientError(f"agent_config contains reserved keys: {overlap}")
76
+ raise AgentClientError(
77
+ f"agent_config contains reserved keys: {overlap}"
78
+ )
86
79
  configurable.update(agent_config)
87
80
 
88
81
  kwargs = {
@@ -92,7 +85,7 @@ class AgentClient:
92
85
  run_id=run_id,
93
86
  ),
94
87
  }
95
-
88
+
96
89
  try:
97
90
  response = await self.agent.ainvoke(**kwargs)
98
91
  output = langchain_to_chat_message(response["messages"][-1])
@@ -124,7 +117,9 @@ class AgentClient:
124
117
  configurable = {"thread_id": thread_id}
125
118
  if agent_config:
126
119
  if overlap := configurable.keys() & agent_config.keys():
127
- raise AgentClientError(f"agent_config contains reserved keys: {overlap}")
120
+ raise AgentClientError(
121
+ f"agent_config contains reserved keys: {overlap}"
122
+ )
128
123
  configurable.update(agent_config)
129
124
 
130
125
  kwargs = {
@@ -134,7 +129,7 @@ class AgentClient:
134
129
  run_id=run_id,
135
130
  ),
136
131
  }
137
-
132
+
138
133
  try:
139
134
  response = self.agent.invoke(**kwargs)
140
135
  output = langchain_to_chat_message(response["messages"][-1])
@@ -146,7 +141,7 @@ class AgentClient:
146
141
  def _parse_stream_line(self, line: str) -> ChatMessage | str | None:
147
142
  """
148
143
  Parse a line from the stream.
149
-
144
+
150
145
  This method is kept for compatibility but is no longer used
151
146
  since we're directly streaming from the agent.
152
147
  """
@@ -201,7 +196,9 @@ class AgentClient:
201
196
  configurable = {"thread_id": thread_id}
202
197
  if agent_config:
203
198
  if overlap := configurable.keys() & agent_config.keys():
204
- raise AgentClientError(f"agent_config contains reserved keys: {overlap}")
199
+ raise AgentClientError(
200
+ f"agent_config contains reserved keys: {overlap}"
201
+ )
205
202
  configurable.update(agent_config)
206
203
 
207
204
  kwargs = {
@@ -211,7 +208,7 @@ class AgentClient:
211
208
  run_id=run_id,
212
209
  ),
213
210
  }
214
-
211
+
215
212
  try:
216
213
  for event in self.agent.stream_events(**kwargs, version="v2"):
217
214
  if not event:
@@ -226,12 +223,16 @@ class AgentClient:
226
223
  and any(t.startswith("graph:step:") for t in event.get("tags", []))
227
224
  ):
228
225
  if isinstance(event["data"]["output"], Command):
229
- new_messages = event["data"]["output"].update.get("messages", [])
226
+ new_messages = event["data"]["output"].update.get(
227
+ "messages", []
228
+ )
230
229
  elif "messages" in event["data"]["output"]:
231
230
  new_messages = event["data"]["output"]["messages"]
232
231
 
233
232
  # Also yield intermediate messages from agents.utils.CustomData.adispatch().
234
- if event["event"] == "on_custom_event" and "custom_data_dispatch" in event.get(
233
+ if event[
234
+ "event"
235
+ ] == "on_custom_event" and "custom_data_dispatch" in event.get(
235
236
  "tags", []
236
237
  ):
237
238
  new_messages = [event["data"]]
@@ -240,14 +241,11 @@ class AgentClient:
240
241
  try:
241
242
  chat_message = langchain_to_chat_message(message)
242
243
  chat_message.run_id = str(run_id)
243
- except Exception as e:
244
+ except Exception:
244
245
  yield f"data: {json.dumps({'type': 'error', 'content': 'Unexpected error'})}\n\n"
245
246
  continue
246
247
  # LangGraph re-sends the input message, which feels weird, so drop it
247
- if (
248
- chat_message.type == "human"
249
- and chat_message.content == message
250
- ):
248
+ if chat_message.type == "human" and chat_message.content == message:
251
249
  continue
252
250
  yield chat_message
253
251
 
@@ -296,7 +294,9 @@ class AgentClient:
296
294
  configurable = {"thread_id": thread_id}
297
295
  if agent_config:
298
296
  if overlap := configurable.keys() & agent_config.keys():
299
- raise AgentClientError(f"agent_config contains reserved keys: {overlap}")
297
+ raise AgentClientError(
298
+ f"agent_config contains reserved keys: {overlap}"
299
+ )
300
300
  configurable.update(agent_config)
301
301
 
302
302
  kwargs = {
@@ -306,7 +306,7 @@ class AgentClient:
306
306
  run_id=run_id,
307
307
  ),
308
308
  }
309
-
309
+
310
310
  try:
311
311
  async for event in self.agent.astream_events(**kwargs, version="v2"):
312
312
  if not event:
@@ -321,12 +321,16 @@ class AgentClient:
321
321
  and any(t.startswith("graph:step:") for t in event.get("tags", []))
322
322
  ):
323
323
  if isinstance(event["data"]["output"], Command):
324
- new_messages = event["data"]["output"].update.get("messages", [])
324
+ new_messages = event["data"]["output"].update.get(
325
+ "messages", []
326
+ )
325
327
  elif "messages" in event["data"]["output"]:
326
328
  new_messages = event["data"]["output"]["messages"]
327
329
 
328
330
  # Also yield intermediate messages from agents.utils.CustomData.adispatch().
329
- if event["event"] == "on_custom_event" and "custom_data_dispatch" in event.get(
331
+ if event[
332
+ "event"
333
+ ] == "on_custom_event" and "custom_data_dispatch" in event.get(
330
334
  "tags", []
331
335
  ):
332
336
  new_messages = [event["data"]]
@@ -335,14 +339,11 @@ class AgentClient:
335
339
  try:
336
340
  chat_message = langchain_to_chat_message(message)
337
341
  chat_message.run_id = str(run_id)
338
- except Exception as e:
342
+ except Exception:
339
343
  yield f"data: {json.dumps({'type': 'error', 'content': 'Unexpected error'})}\n\n"
340
344
  continue
341
345
  # LangGraph re-sends the input message, which feels weird, so drop it
342
- if (
343
- chat_message.type == "human"
344
- and chat_message.content == message
345
- ):
346
+ if chat_message.type == "human" and chat_message.content == message:
346
347
  continue
347
348
  yield chat_message
348
349
 
@@ -380,9 +381,7 @@ class AgentClient:
380
381
  )
381
382
  )
382
383
  messages = state_snapshot.values["messages"]
383
- chat_messages = [
384
- langchain_to_chat_message(m) for m in messages
385
- ]
384
+ chat_messages = [langchain_to_chat_message(m) for m in messages]
386
385
  return ChatHistory(messages=chat_messages)
387
386
  except Exception as e:
388
387
  raise AgentClientError(f"Error getting history: {e}") from e
@@ -6,7 +6,6 @@ from collections.abc import AsyncGenerator
6
6
  from pathlib import Path
7
7
 
8
8
  import streamlit as st
9
- from dotenv import load_dotenv
10
9
  from pydantic import ValidationError
11
10
  from streamlit.runtime.scriptrunner import get_script_run_ctx
12
11
 
@@ -21,6 +20,7 @@ APP_TITLE = "MCP Playground"
21
20
  APP_ICON = "🧰"
22
21
  use_streaming = True
23
22
 
23
+
24
24
  # --- Function to handle unique filename generation ---
25
25
  def get_unique_filepath(upload_dir: Path, filename: str) -> Path:
26
26
  """Checks if a file exists and returns a unique path if needed."""
@@ -38,6 +38,7 @@ def get_unique_filepath(upload_dir: Path, filename: str) -> Path:
38
38
  return new_filepath
39
39
  counter += 1
40
40
 
41
+
41
42
  async def main() -> None:
42
43
  # Configure page layout
43
44
  st.set_page_config(
@@ -49,7 +50,8 @@ async def main() -> None:
49
50
  )
50
51
 
51
52
  # Custom CSS for chat layout and styling
52
- st.markdown("""
53
+ st.markdown(
54
+ """
53
55
  <style>
54
56
  /* Hide Streamlit default header and footer */
55
57
  #MainMenu, header, footer {
@@ -158,7 +160,9 @@ async def main() -> None:
158
160
  border-bottom-left-radius: 0.25rem;
159
161
  }
160
162
  </style>
161
- """, unsafe_allow_html=True)
163
+ """,
164
+ unsafe_allow_html=True,
165
+ )
162
166
 
163
167
  # Minimal toolbar
164
168
  if st.get_option("client.toolbarMode") != "minimal":
@@ -218,7 +222,7 @@ async def main() -> None:
218
222
  "Upload a file to process",
219
223
  type=None,
220
224
  key="file_uploader",
221
- help="Upload any file to process with the AI assistant"
225
+ help="Upload any file to process with the AI assistant",
222
226
  )
223
227
 
224
228
  # Handle file upload state
@@ -234,7 +238,8 @@ async def main() -> None:
234
238
  icon="📄",
235
239
  )
236
240
  elif (
237
- uploaded_file is None and st.session_state.get("uploaded_file_obj") is not None
241
+ uploaded_file is None
242
+ and st.session_state.get("uploaded_file_obj") is not None
238
243
  ):
239
244
  st.session_state.uploaded_file_obj = None
240
245
  st.session_state.file_processed = False
@@ -260,19 +265,21 @@ async def main() -> None:
260
265
  yield m
261
266
 
262
267
  await draw_messages(amessage_iter())
263
- st.markdown('</div>', unsafe_allow_html=True)
268
+ st.markdown("</div>", unsafe_allow_html=True)
264
269
 
265
270
  # Chat input area
266
271
  st.markdown('<div class="chat-input-area">', unsafe_allow_html=True)
267
272
  if user_input := st.chat_input(
268
- "Enter message or upload a file and describe task...",
269
- key="chat_input"
273
+ "Enter message or upload a file and describe task...", key="chat_input"
270
274
  ):
271
275
  final_message_content = user_input
272
276
  display_content = user_input
273
277
 
274
278
  # --- Handle File Upload Integration ---
275
- if st.session_state.uploaded_file_obj and not st.session_state.file_processed:
279
+ if (
280
+ st.session_state.uploaded_file_obj
281
+ and not st.session_state.file_processed
282
+ ):
276
283
  uploaded_file_obj = st.session_state.uploaded_file_obj
277
284
  original_filename = uploaded_file_obj.name
278
285
  try:
@@ -322,10 +329,11 @@ async def main() -> None:
322
329
  st.error(
323
330
  f"An unexpected error occurred during agent communication: {e}"
324
331
  )
325
- st.markdown('</div>', unsafe_allow_html=True)
332
+ st.markdown("</div>", unsafe_allow_html=True)
326
333
 
327
334
  # End chat app wrapper
328
- st.markdown('</div>', unsafe_allow_html=True)
335
+ st.markdown("</div>", unsafe_allow_html=True)
336
+
329
337
 
330
338
  async def draw_messages(
331
339
  messages_agen: AsyncGenerator[ChatMessage | str, None],
@@ -419,6 +427,7 @@ async def draw_messages(
419
427
  st.write(msg)
420
428
  st.stop()
421
429
 
430
+
422
431
  async def handle_feedback() -> None:
423
432
  """Draws a feedback widget and records feedback from the user."""
424
433
  if "last_feedback" not in st.session_state:
@@ -444,6 +453,7 @@ async def handle_feedback() -> None:
444
453
  st.session_state.last_feedback = (latest_run_id, feedback)
445
454
  st.toast("Feedback recorded", icon="⭐")
446
455
 
456
+
447
457
  if __name__ == "__main__":
448
458
  asyncio.run(main())
449
459
  # End of Selectio
@@ -53,7 +53,7 @@ def sample_schema(temp_dir):
53
53
  @pytest.mark.asyncio
54
54
  async def test_generate_api_without_output(sample_schema):
55
55
  """Test API generation without output file (return code only)."""
56
- result =generate_api_from_schema(
56
+ result = generate_api_from_schema(
57
57
  schema_path=sample_schema, output_path=None, add_docstrings=False
58
58
  )
59
59
 
@@ -76,8 +76,8 @@ async def test_generate_api_with_output(sample_schema, temp_dir):
76
76
  schema_path=sample_schema, output_path=output_path, add_docstrings=True
77
77
  )
78
78
 
79
- assert "app_file" is not None
80
- assert "readme_file" is not None
79
+ assert "app_file" != None
80
+ assert "readme_file" != None
81
81
 
82
82
  assert app_file.exists()
83
83
  content = app_file.read_text()
@@ -138,7 +138,6 @@ async def test_generate_api_with_docstrings(sample_schema, temp_dir):
138
138
  assert "Tags:" in content
139
139
 
140
140
 
141
-
142
141
  @pytest.mark.asyncio
143
142
  async def test_generate_api_without_docstrings(sample_schema, temp_dir):
144
143
  """Test API generation without docstring generation."""
@@ -1,6 +1,6 @@
1
1
  import pytest
2
2
 
3
- from universal_mcp.applications.perplexity.app import PerplexityApp
3
+ from universal_mcp.applications import app_from_slug
4
4
  from universal_mcp.exceptions import NotAuthorizedError
5
5
  from universal_mcp.integrations import ApiKeyIntegration
6
6
  from universal_mcp.stores import MemoryStore
@@ -14,7 +14,7 @@ def test_perplexity_api_no_key():
14
14
  integration = ApiKeyIntegration("PERPLEXITY", store=store)
15
15
 
16
16
  # Create Perplexity app with the integration
17
- app = PerplexityApp(integration=integration)
17
+ app = app_from_slug("perplexity")(integration=integration)
18
18
 
19
19
  # Try to make a chat request without setting API key
20
20
  with pytest.raises(NotAuthorizedError) as exc_info:
@@ -59,4 +59,4 @@ def test_application(app_name):
59
59
  assert isinstance(tools[0], Callable)
60
60
  for tool in tools:
61
61
  assert tool.__name__ is not None
62
- assert tool.__doc__ is not None
62
+ assert tool.__doc__ is not None
@@ -67,6 +67,7 @@ class TestEnvironmentStore:
67
67
 
68
68
 
69
69
  # Test KeyringStore
70
+ @pytest.mark.skip(reason="Skipping KeyringStore tests")
70
71
  class TestKeyringStore:
71
72
  @pytest.fixture
72
73
  def store(self):
@@ -1,4 +1,5 @@
1
1
  import importlib
2
+ import subprocess
2
3
 
3
4
  from loguru import logger
4
5
 
@@ -7,12 +8,12 @@ from universal_mcp.applications.application import (
7
8
  BaseApplication,
8
9
  GraphQLApplication,
9
10
  )
10
- import subprocess
11
11
 
12
12
  # Name are in the format of "app-name", eg, google-calendar
13
13
  # Folder name is "app_name", eg, google_calendar
14
14
  # Class name is NameApp, eg, GoogleCalendarApp
15
15
 
16
+
16
17
  def _import_class(module_path: str, class_name: str):
17
18
  """
18
19
  Helper to import a class by name from a module.
@@ -27,7 +28,10 @@ def _import_class(module_path: str, class_name: str):
27
28
  return getattr(module, class_name)
28
29
  except AttributeError as e:
29
30
  logger.error(f"Class '{class_name}' not found in module '{module_path}'")
30
- raise ModuleNotFoundError(f"Class '{class_name}' not found in module '{module_path}'") from e
31
+ raise ModuleNotFoundError(
32
+ f"Class '{class_name}' not found in module '{module_path}'"
33
+ ) from e
34
+
31
35
 
32
36
  def _install_package(slug_clean: str):
33
37
  """
@@ -40,10 +44,13 @@ def _install_package(slug_clean: str):
40
44
  subprocess.check_call(cmd)
41
45
  except subprocess.CalledProcessError as e:
42
46
  logger.error(f"Installation failed for '{slug_clean}': {e}")
43
- raise ModuleNotFoundError(f"Installation failed for package '{slug_clean}'") from e
47
+ raise ModuleNotFoundError(
48
+ f"Installation failed for package '{slug_clean}'"
49
+ ) from e
44
50
  else:
45
51
  logger.info(f"Package '{slug_clean}' installed successfully")
46
52
 
53
+
47
54
  def app_from_slug(slug: str):
48
55
  """
49
56
  Dynamically resolve and return the application class for the given slug.
@@ -54,19 +61,26 @@ def app_from_slug(slug: str):
54
61
  package_prefix = f"universal_mcp_{slug_clean.replace('-', '_')}"
55
62
  module_path = f"{package_prefix}.app"
56
63
 
57
- logger.info(f"Resolving app for slug '{slug}' → module '{module_path}', class '{class_name}'")
64
+ logger.info(
65
+ f"Resolving app for slug '{slug}' → module '{module_path}', class '{class_name}'"
66
+ )
58
67
  try:
59
68
  return _import_class(module_path, class_name)
60
69
  except ModuleNotFoundError as orig_err:
61
- logger.warning(f"Module '{module_path}' not found locally: {orig_err}. Installing...")
70
+ logger.warning(
71
+ f"Module '{module_path}' not found locally: {orig_err}. Installing..."
72
+ )
62
73
  _install_package(slug_clean)
63
74
  # Retry import after installation
64
75
  try:
65
76
  return _import_class(module_path, class_name)
66
77
  except ModuleNotFoundError as retry_err:
67
- logger.error(f"Still cannot import '{module_path}' after installation: {retry_err}")
78
+ logger.error(
79
+ f"Still cannot import '{module_path}' after installation: {retry_err}"
80
+ )
68
81
  raise
69
82
 
83
+
70
84
  __all__ = [
71
85
  "app_from_slug",
72
86
  "BaseApplication",