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.
- {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc14}/PKG-INFO +2 -53
- universal_mcp-0.1.13rc14/pyproject.toml +127 -0
- {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc14}/src/playground/__main__.py +3 -1
- {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc14}/src/playground/client.py +40 -41
- {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc14}/src/playground/streamlit.py +21 -10
- {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc14}/src/tests/test_api_generator.py +9 -55
- {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc14}/src/tests/test_api_integration.py +2 -2
- {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc14}/src/tests/test_applications.py +5 -1
- {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc14}/src/tests/test_stores.py +1 -0
- {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc14}/src/universal_mcp/applications/__init__.py +32 -8
- {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc14}/src/universal_mcp/cli.py +41 -27
- {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc14}/src/universal_mcp/servers/server.py +0 -2
- {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc14}/src/universal_mcp/stores/store.py +2 -0
- {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc14}/src/universal_mcp/tools/tools.py +1 -3
- {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc14}/src/universal_mcp/utils/agentr.py +8 -3
- {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc14}/src/universal_mcp/utils/api_generator.py +14 -101
- {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc14}/src/universal_mcp/utils/docgen.py +2 -2
- universal_mcp-0.1.13rc14/src/universal_mcp/utils/openapi.py +697 -0
- universal_mcp-0.1.13rc14/src/universal_mcp/utils/readme.py +92 -0
- universal_mcp-0.1.13rc3/pyproject.toml +0 -241
- universal_mcp-0.1.13rc3/src/playground/memory/__init__.py +0 -14
- universal_mcp-0.1.13rc3/src/playground/memory/sqlite.py +0 -9
- universal_mcp-0.1.13rc3/src/universal_mcp/utils/openapi.py +0 -455
- {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc14}/.gitignore +0 -0
- {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc14}/README.md +0 -0
- {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc14}/src/playground/README.md +0 -0
- {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc14}/src/playground/__init__.py +0 -0
- {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc14}/src/playground/agents/react.py +0 -0
- {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc14}/src/playground/schema.py +0 -0
- {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc14}/src/playground/settings.py +0 -0
- {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc14}/src/playground/utils.py +0 -0
- {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc14}/src/tests/__init__.py +0 -0
- {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc14}/src/tests/conftest.py +0 -0
- {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc14}/src/tests/test_localserver.py +0 -0
- {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc14}/src/tests/test_tool.py +0 -0
- {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc14}/src/tests/test_zenquotes.py +0 -0
- {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc14}/src/universal_mcp/__init__.py +0 -0
- {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc14}/src/universal_mcp/analytics.py +0 -0
- {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc14}/src/universal_mcp/applications/application.py +0 -0
- {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc14}/src/universal_mcp/config.py +0 -0
- {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc14}/src/universal_mcp/exceptions.py +0 -0
- {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc14}/src/universal_mcp/integrations/README.md +0 -0
- {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc14}/src/universal_mcp/integrations/__init__.py +1 -1
- {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc14}/src/universal_mcp/integrations/integration.py +0 -0
- {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc14}/src/universal_mcp/logger.py +0 -0
- {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc14}/src/universal_mcp/py.typed +0 -0
- {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc14}/src/universal_mcp/servers/README.md +0 -0
- {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc14}/src/universal_mcp/servers/__init__.py +0 -0
- {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc14}/src/universal_mcp/stores/README.md +0 -0
- {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc14}/src/universal_mcp/stores/__init__.py +0 -0
- {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc14}/src/universal_mcp/templates/README.md.j2 +0 -0
- {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc14}/src/universal_mcp/templates/api_client.py.j2 +0 -0
- {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc14}/src/universal_mcp/tools/README.md +0 -0
- {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc14}/src/universal_mcp/tools/__init__.py +0 -0
- {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc14}/src/universal_mcp/tools/adapters.py +0 -0
- {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc14}/src/universal_mcp/tools/func_metadata.py +0 -0
- {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc14}/src/universal_mcp/utils/__init__.py +0 -0
- {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc14}/src/universal_mcp/utils/docstring_parser.py +0 -0
- {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc14}/src/universal_mcp/utils/dump_app_tools.py +0 -0
- {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc14}/src/universal_mcp/utils/installation.py +0 -0
- {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.
|
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(
|
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
|
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
|
-
|
21
|
+
|
26
22
|
|
27
23
|
@asynccontextmanager
|
28
24
|
async def create_agent_client():
|
29
|
-
async with create_agent() as react_agent
|
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(
|
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(
|
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(
|
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(
|
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[
|
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
|
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(
|
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(
|
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[
|
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
|
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
|
-
""",
|
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
|
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(
|
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
|
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(
|
332
|
+
st.markdown("</div>", unsafe_allow_html=True)
|
325
333
|
|
326
334
|
# End chat app wrapper
|
327
|
-
st.markdown(
|
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
|
76
|
-
schema_path=sample_schema, output_path=output_path
|
73
|
+
app_file = generate_api_from_schema(
|
74
|
+
schema_path=sample_schema, output_path=output_path
|
77
75
|
)
|
78
76
|
|
79
|
-
assert "app_file"
|
80
|
-
assert "readme_file"
|
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
|
148
|
-
schema_path=sample_schema, output_path=output_path
|
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
|
208
|
-
schema_path=schema_file, output_path=output_path
|
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
|
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 =
|
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
|