universal-mcp 0.1.13rc3__tar.gz → 0.1.13rc14__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 (61) hide show
  1. {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc14}/PKG-INFO +2 -53
  2. universal_mcp-0.1.13rc14/pyproject.toml +127 -0
  3. {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc14}/src/playground/__main__.py +3 -1
  4. {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc14}/src/playground/client.py +40 -41
  5. {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc14}/src/playground/streamlit.py +21 -10
  6. {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc14}/src/tests/test_api_generator.py +9 -55
  7. {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc14}/src/tests/test_api_integration.py +2 -2
  8. {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc14}/src/tests/test_applications.py +5 -1
  9. {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc14}/src/tests/test_stores.py +1 -0
  10. {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc14}/src/universal_mcp/applications/__init__.py +32 -8
  11. {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc14}/src/universal_mcp/cli.py +41 -27
  12. {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc14}/src/universal_mcp/servers/server.py +0 -2
  13. {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc14}/src/universal_mcp/stores/store.py +2 -0
  14. {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc14}/src/universal_mcp/tools/tools.py +1 -3
  15. {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc14}/src/universal_mcp/utils/agentr.py +8 -3
  16. {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc14}/src/universal_mcp/utils/api_generator.py +14 -101
  17. {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc14}/src/universal_mcp/utils/docgen.py +2 -2
  18. universal_mcp-0.1.13rc14/src/universal_mcp/utils/openapi.py +697 -0
  19. universal_mcp-0.1.13rc14/src/universal_mcp/utils/readme.py +92 -0
  20. universal_mcp-0.1.13rc3/pyproject.toml +0 -241
  21. universal_mcp-0.1.13rc3/src/playground/memory/__init__.py +0 -14
  22. universal_mcp-0.1.13rc3/src/playground/memory/sqlite.py +0 -9
  23. universal_mcp-0.1.13rc3/src/universal_mcp/utils/openapi.py +0 -455
  24. {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc14}/.gitignore +0 -0
  25. {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc14}/README.md +0 -0
  26. {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc14}/src/playground/README.md +0 -0
  27. {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc14}/src/playground/__init__.py +0 -0
  28. {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc14}/src/playground/agents/react.py +0 -0
  29. {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc14}/src/playground/schema.py +0 -0
  30. {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc14}/src/playground/settings.py +0 -0
  31. {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc14}/src/playground/utils.py +0 -0
  32. {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc14}/src/tests/__init__.py +0 -0
  33. {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc14}/src/tests/conftest.py +0 -0
  34. {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc14}/src/tests/test_localserver.py +0 -0
  35. {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc14}/src/tests/test_tool.py +0 -0
  36. {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc14}/src/tests/test_zenquotes.py +0 -0
  37. {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc14}/src/universal_mcp/__init__.py +0 -0
  38. {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc14}/src/universal_mcp/analytics.py +0 -0
  39. {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc14}/src/universal_mcp/applications/application.py +0 -0
  40. {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc14}/src/universal_mcp/config.py +0 -0
  41. {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc14}/src/universal_mcp/exceptions.py +0 -0
  42. {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc14}/src/universal_mcp/integrations/README.md +0 -0
  43. {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc14}/src/universal_mcp/integrations/__init__.py +1 -1
  44. {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc14}/src/universal_mcp/integrations/integration.py +0 -0
  45. {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc14}/src/universal_mcp/logger.py +0 -0
  46. {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc14}/src/universal_mcp/py.typed +0 -0
  47. {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc14}/src/universal_mcp/servers/README.md +0 -0
  48. {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc14}/src/universal_mcp/servers/__init__.py +0 -0
  49. {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc14}/src/universal_mcp/stores/README.md +0 -0
  50. {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc14}/src/universal_mcp/stores/__init__.py +0 -0
  51. {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc14}/src/universal_mcp/templates/README.md.j2 +0 -0
  52. {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc14}/src/universal_mcp/templates/api_client.py.j2 +0 -0
  53. {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc14}/src/universal_mcp/tools/README.md +0 -0
  54. {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc14}/src/universal_mcp/tools/__init__.py +0 -0
  55. {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc14}/src/universal_mcp/tools/adapters.py +0 -0
  56. {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc14}/src/universal_mcp/tools/func_metadata.py +0 -0
  57. {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc14}/src/universal_mcp/utils/__init__.py +0 -0
  58. {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc14}/src/universal_mcp/utils/docstring_parser.py +0 -0
  59. {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc14}/src/universal_mcp/utils/dump_app_tools.py +0 -0
  60. {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc14}/src/universal_mcp/utils/installation.py +0 -0
  61. {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc14}/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.13rc3
3
+ Version: 0.1.13rc14
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
@@ -18,60 +18,9 @@ Requires-Dist: pydantic>=2.11.1
18
18
  Requires-Dist: pyyaml>=6.0.2
19
19
  Requires-Dist: rich>=14.0.0
20
20
  Requires-Dist: typer>=0.15.2
21
- Provides-Extra: all
22
- Requires-Dist: langchain-mcp-adapters>=0.0.3; extra == 'all'
23
- Requires-Dist: langchain-openai>=0.3.12; extra == 'all'
24
- Requires-Dist: langgraph-checkpoint-sqlite>=2.0.6; extra == 'all'
25
- Requires-Dist: langgraph>=0.3.24; extra == 'all'
26
- Requires-Dist: litellm>=1.30.7; extra == 'all'
27
- Requires-Dist: pyright>=1.1.398; extra == 'all'
28
- Requires-Dist: pytest-asyncio>=0.26.0; extra == 'all'
29
- Requires-Dist: pytest>=8.3.5; extra == 'all'
30
- Requires-Dist: python-dotenv>=1.0.1; extra == 'all'
31
- Requires-Dist: ruff>=0.11.4; extra == 'all'
32
- Requires-Dist: streamlit>=1.44.1; extra == 'all'
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'
73
21
  Provides-Extra: dev
74
22
  Requires-Dist: litellm>=1.30.7; extra == 'dev'
23
+ Requires-Dist: pre-commit>=4.2.0; extra == 'dev'
75
24
  Requires-Dist: pyright>=1.1.398; extra == 'dev'
76
25
  Requires-Dist: pytest-asyncio>=0.26.0; extra == 'dev'
77
26
  Requires-Dist: pytest>=8.3.5; extra == 'dev'
@@ -0,0 +1,127 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "universal-mcp"
7
+ version = "0.1.13-rc14"
8
+ 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."
9
+ readme = "README.md"
10
+ authors = [
11
+ { name = "Manoj Bajaj", email = "manojbajaj95@gmail.com" }
12
+ ]
13
+ requires-python = ">=3.11"
14
+ license = { text = "MIT" }
15
+ dependencies = [
16
+ "Jinja2>=3.1.3",
17
+ "cookiecutter>=2.6.0",
18
+ "gql[all]>=3.5.2",
19
+ "keyring>=25.6.0",
20
+ "litellm>=1.30.7",
21
+ "loguru>=0.7.3",
22
+ "mcp>=1.6.0",
23
+ "posthog>=3.24.0",
24
+ "pydantic>=2.11.1",
25
+ "pydantic-settings>=2.8.1",
26
+ "pyyaml>=6.0.2",
27
+ "rich>=14.0.0",
28
+ "typer>=0.15.2",
29
+ ]
30
+
31
+ [project.optional-dependencies]
32
+ dev = [
33
+ "litellm>=1.30.7",
34
+ "pre-commit>=4.2.0",
35
+ "pyright>=1.1.398",
36
+ "pytest>=8.3.5",
37
+ "pytest-asyncio>=0.26.0",
38
+ "ruff>=0.11.4",
39
+ ]
40
+ playground = [
41
+ "langchain-mcp-adapters>=0.0.3",
42
+ "langchain-openai>=0.3.12",
43
+ "langgraph>=0.3.24",
44
+ "langgraph-checkpoint-sqlite>=2.0.6",
45
+ "python-dotenv>=1.0.1",
46
+ "streamlit>=1.44.1",
47
+ "watchdog>=6.0.0",
48
+ ]
49
+
50
+ [project.scripts]
51
+ universal_mcp = "universal_mcp.cli:app"
52
+
53
+ # ------------------------------
54
+ # Hatch build configuration
55
+ # ------------------------------
56
+ [tool.hatch.build.targets.sdist]
57
+ include = [
58
+ "/src",
59
+ "/templates", # Important: Include the templates directory!
60
+ ]
61
+
62
+ [tool.hatch.build.targets.wheel]
63
+ packages = ["src/universal_mcp"]
64
+
65
+ [tool.hatch.build.targets.wheel.shared-data]
66
+ "templates" = "universal_mcp/templates"
67
+
68
+ # ------------------------------
69
+ # Tool configurations
70
+ # ------------------------------
71
+ [tool.ruff]
72
+ exclude = [
73
+ ".bzr",
74
+ ".direnv",
75
+ ".eggs",
76
+ ".git",
77
+ ".git-rewrite",
78
+ ".hg",
79
+ ".ipynb_checkpoints",
80
+ ".mypy_cache",
81
+ ".nox",
82
+ ".pants.d",
83
+ ".pyenv",
84
+ ".pytest_cache",
85
+ ".pytype",
86
+ ".ruff_cache",
87
+ ".svn",
88
+ ".tox",
89
+ ".venv",
90
+ ".vscode",
91
+ "__pypackages__",
92
+ "_build",
93
+ "buck-out",
94
+ "build",
95
+ "dist",
96
+ "node_modules",
97
+ "site-packages",
98
+ "venv",
99
+ ]
100
+ line-length = 88
101
+ indent-width = 4
102
+ target-version = "py312"
103
+
104
+ [tool.ruff.lint]
105
+ select = [
106
+ "E", # pycodestyle
107
+ "F", # Pyflakes
108
+ "UP", # pyupgrade
109
+ "B", # flake8-bugbear
110
+ "SIM", # flake8-simplify
111
+ "I", # isort
112
+ ]
113
+ ignore = ['E501', 'B008']
114
+ fixable = ["ALL"]
115
+ unfixable = []
116
+ dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
117
+
118
+ [tool.ruff.format]
119
+ quote-style = "double"
120
+ indent-style = "space"
121
+ skip-magic-trailing-comma = false
122
+ line-ending = "auto"
123
+ docstring-code-line-length = "dynamic"
124
+
125
+ [tool.pytest.ini_options]
126
+ asyncio_mode = "strict"
127
+ asyncio_default_fixture_loop_scope = "function"
@@ -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")
@@ -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
@@ -20,6 +20,7 @@ APP_TITLE = "MCP Playground"
20
20
  APP_ICON = "🧰"
21
21
  use_streaming = True
22
22
 
23
+
23
24
  # --- Function to handle unique filename generation ---
24
25
  def get_unique_filepath(upload_dir: Path, filename: str) -> Path:
25
26
  """Checks if a file exists and returns a unique path if needed."""
@@ -37,6 +38,7 @@ def get_unique_filepath(upload_dir: Path, filename: str) -> Path:
37
38
  return new_filepath
38
39
  counter += 1
39
40
 
41
+
40
42
  async def main() -> None:
41
43
  # Configure page layout
42
44
  st.set_page_config(
@@ -48,7 +50,8 @@ async def main() -> None:
48
50
  )
49
51
 
50
52
  # Custom CSS for chat layout and styling
51
- st.markdown("""
53
+ st.markdown(
54
+ """
52
55
  <style>
53
56
  /* Hide Streamlit default header and footer */
54
57
  #MainMenu, header, footer {
@@ -157,7 +160,9 @@ async def main() -> None:
157
160
  border-bottom-left-radius: 0.25rem;
158
161
  }
159
162
  </style>
160
- """, unsafe_allow_html=True)
163
+ """,
164
+ unsafe_allow_html=True,
165
+ )
161
166
 
162
167
  # Minimal toolbar
163
168
  if st.get_option("client.toolbarMode") != "minimal":
@@ -217,7 +222,7 @@ async def main() -> None:
217
222
  "Upload a file to process",
218
223
  type=None,
219
224
  key="file_uploader",
220
- help="Upload any file to process with the AI assistant"
225
+ help="Upload any file to process with the AI assistant",
221
226
  )
222
227
 
223
228
  # Handle file upload state
@@ -233,7 +238,8 @@ async def main() -> None:
233
238
  icon="📄",
234
239
  )
235
240
  elif (
236
- 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
237
243
  ):
238
244
  st.session_state.uploaded_file_obj = None
239
245
  st.session_state.file_processed = False
@@ -259,19 +265,21 @@ async def main() -> None:
259
265
  yield m
260
266
 
261
267
  await draw_messages(amessage_iter())
262
- st.markdown('</div>', unsafe_allow_html=True)
268
+ st.markdown("</div>", unsafe_allow_html=True)
263
269
 
264
270
  # Chat input area
265
271
  st.markdown('<div class="chat-input-area">', unsafe_allow_html=True)
266
272
  if user_input := st.chat_input(
267
- "Enter message or upload a file and describe task...",
268
- key="chat_input"
273
+ "Enter message or upload a file and describe task...", key="chat_input"
269
274
  ):
270
275
  final_message_content = user_input
271
276
  display_content = user_input
272
277
 
273
278
  # --- Handle File Upload Integration ---
274
- 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
+ ):
275
283
  uploaded_file_obj = st.session_state.uploaded_file_obj
276
284
  original_filename = uploaded_file_obj.name
277
285
  try:
@@ -321,10 +329,11 @@ async def main() -> None:
321
329
  st.error(
322
330
  f"An unexpected error occurred during agent communication: {e}"
323
331
  )
324
- st.markdown('</div>', unsafe_allow_html=True)
332
+ st.markdown("</div>", unsafe_allow_html=True)
325
333
 
326
334
  # End chat app wrapper
327
- st.markdown('</div>', unsafe_allow_html=True)
335
+ st.markdown("</div>", unsafe_allow_html=True)
336
+
328
337
 
329
338
  async def draw_messages(
330
339
  messages_agen: AsyncGenerator[ChatMessage | str, None],
@@ -418,6 +427,7 @@ async def draw_messages(
418
427
  st.write(msg)
419
428
  st.stop()
420
429
 
430
+
421
431
  async def handle_feedback() -> None:
422
432
  """Draws a feedback widget and records feedback from the user."""
423
433
  if "last_feedback" not in st.session_state:
@@ -443,6 +453,7 @@ async def handle_feedback() -> None:
443
453
  st.session_state.last_feedback = (latest_run_id, feedback)
444
454
  st.toast("Feedback recorded", icon="⭐")
445
455
 
456
+
446
457
  if __name__ == "__main__":
447
458
  asyncio.run(main())
448
459
  # End of Selectio
@@ -53,9 +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(
57
- schema_path=sample_schema, output_path=None, add_docstrings=False
58
- )
56
+ result = generate_api_from_schema(schema_path=sample_schema, output_path=None)
59
57
 
60
58
  assert "code" in result
61
59
  assert isinstance(result["code"], str)
@@ -72,12 +70,12 @@ async def test_generate_api_with_output(sample_schema, temp_dir):
72
70
  """Test API generation with output file."""
73
71
  output_path = temp_dir / "test.py"
74
72
 
75
- app_file, readme_file = generate_api_from_schema(
76
- schema_path=sample_schema, output_path=output_path, add_docstrings=True
73
+ app_file = generate_api_from_schema(
74
+ schema_path=sample_schema, output_path=output_path
77
75
  )
78
76
 
79
- assert "app_file" is not None
80
- assert "readme_file" is not None
77
+ assert "app_file" != None
78
+ assert "readme_file" != None
81
79
 
82
80
  assert app_file.exists()
83
81
  content = app_file.read_text()
@@ -87,14 +85,6 @@ async def test_generate_api_with_output(sample_schema, temp_dir):
87
85
  assert "def test_operation" in content
88
86
  assert "def list_tools" in content
89
87
 
90
- # Verify README exists and contains expected content
91
- if readme_file:
92
- assert readme_file.exists()
93
- readme_content = readme_file.read_text()
94
- assert "Test MCP Server" in readme_content
95
- assert "Tool List" in readme_content
96
- assert "test_operation" in readme_content
97
-
98
88
 
99
89
  @pytest.mark.asyncio
100
90
  async def test_generate_api_invalid_schema(temp_dir):
@@ -115,41 +105,16 @@ async def test_generate_api_nonexistent_schema():
115
105
  )
116
106
 
117
107
 
118
- @pytest.mark.asyncio
119
- async def test_generate_api_with_docstrings(sample_schema, temp_dir):
120
- """Test API generation with docstring generation."""
121
- output_path = temp_dir / "test_with_docs.py"
122
-
123
- app_file, readme_file = generate_api_from_schema(
124
- schema_path=sample_schema, output_path=output_path, add_docstrings=True
125
- )
126
-
127
- assert app_file is not None
128
- assert readme_file is not None
129
- assert app_file.exists()
130
-
131
- # Check if docstrings were added
132
- content = app_file.read_text()
133
- # Check for required imports and class structure
134
- assert "from universal_mcp.applications import APIApplication" in content
135
- assert "from universal_mcp.integrations import Integration" in content
136
- assert "def test_operation" in content
137
- assert '"""' in content # Basic check for docstring presence
138
- assert "Tags:" in content
139
-
140
-
141
-
142
108
  @pytest.mark.asyncio
143
109
  async def test_generate_api_without_docstrings(sample_schema, temp_dir):
144
110
  """Test API generation without docstring generation."""
145
111
  output_path = temp_dir / "test_without_docs.py"
146
112
 
147
- app_file, readme_file = generate_api_from_schema(
148
- schema_path=sample_schema, output_path=output_path, add_docstrings=False
113
+ app_file = generate_api_from_schema(
114
+ schema_path=sample_schema, output_path=output_path
149
115
  )
150
116
 
151
117
  assert app_file is not None
152
- assert readme_file is not None
153
118
  assert app_file.exists()
154
119
 
155
120
  # Verify the app was generated
@@ -204,12 +169,11 @@ async def test_generate_api_with_complex_schema(temp_dir):
204
169
  json.dump(schema, f)
205
170
 
206
171
  output_path = temp_dir / "complex.py"
207
- app_file, readme_file = generate_api_from_schema(
208
- schema_path=schema_file, output_path=output_path, add_docstrings=True
172
+ app_file = generate_api_from_schema(
173
+ schema_path=schema_file, output_path=output_path
209
174
  )
210
175
 
211
176
  assert app_file is not None
212
- assert readme_file is not None
213
177
  assert app_file.exists()
214
178
 
215
179
  content = app_file.read_text()
@@ -232,13 +196,3 @@ async def test_generate_api_with_complex_schema(temp_dir):
232
196
 
233
197
  # Check for proper typing imports
234
198
  assert "from typing import" in content
235
-
236
- # Verify README was generated
237
- readme_content = readme_file.read_text()
238
-
239
- # Check README content
240
- assert "Complex MCP Server" in readme_content
241
- assert "Tool List" in readme_content
242
- assert "list_users" in readme_content
243
- assert "create_user" in readme_content
244
- assert "get_user" in readme_content
@@ -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:
@@ -57,6 +57,10 @@ def test_application(app_name):
57
57
  logger.info(f"Tools for {app_name}: {tools}")
58
58
  assert len(tools) > 0
59
59
  assert isinstance(tools[0], Callable)
60
+ important_tools = []
60
61
  for tool in tools:
61
62
  assert tool.__name__ is not None
62
- assert tool.__doc__ is not None
63
+ assert tool.__doc__ is not None
64
+ if "important" in tool.__doc__:
65
+ important_tools.append(tool.__name__)
66
+ assert len(important_tools) > 0