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.
- {universal_mcp-0.1.13rc2 → universal_mcp-0.1.13rc7}/PKG-INFO +40 -1
- {universal_mcp-0.1.13rc2 → universal_mcp-0.1.13rc7}/pyproject.toml +41 -1
- {universal_mcp-0.1.13rc2 → universal_mcp-0.1.13rc7}/src/playground/__main__.py +3 -1
- {universal_mcp-0.1.13rc2 → universal_mcp-0.1.13rc7}/src/playground/agents/react.py +1 -1
- {universal_mcp-0.1.13rc2 → universal_mcp-0.1.13rc7}/src/playground/client.py +40 -41
- {universal_mcp-0.1.13rc2 → universal_mcp-0.1.13rc7}/src/playground/streamlit.py +21 -11
- {universal_mcp-0.1.13rc2 → universal_mcp-0.1.13rc7}/src/tests/test_api_generator.py +3 -4
- {universal_mcp-0.1.13rc2 → universal_mcp-0.1.13rc7}/src/tests/test_api_integration.py +2 -2
- {universal_mcp-0.1.13rc2 → universal_mcp-0.1.13rc7}/src/tests/test_applications.py +1 -1
- {universal_mcp-0.1.13rc2 → universal_mcp-0.1.13rc7}/src/tests/test_stores.py +1 -0
- {universal_mcp-0.1.13rc2 → universal_mcp-0.1.13rc7}/src/universal_mcp/applications/__init__.py +20 -6
- {universal_mcp-0.1.13rc2 → universal_mcp-0.1.13rc7}/src/universal_mcp/cli.py +14 -16
- {universal_mcp-0.1.13rc2 → universal_mcp-0.1.13rc7}/src/universal_mcp/servers/server.py +0 -2
- {universal_mcp-0.1.13rc2 → universal_mcp-0.1.13rc7}/src/universal_mcp/stores/store.py +2 -0
- {universal_mcp-0.1.13rc2 → universal_mcp-0.1.13rc7}/src/universal_mcp/tools/tools.py +1 -3
- {universal_mcp-0.1.13rc2 → universal_mcp-0.1.13rc7}/src/universal_mcp/utils/agentr.py +8 -3
- {universal_mcp-0.1.13rc2 → universal_mcp-0.1.13rc7}/src/universal_mcp/utils/api_generator.py +26 -23
- {universal_mcp-0.1.13rc2 → universal_mcp-0.1.13rc7}/src/universal_mcp/utils/installation.py +8 -8
- {universal_mcp-0.1.13rc2 → universal_mcp-0.1.13rc7}/src/universal_mcp/utils/openapi.py +54 -38
- universal_mcp-0.1.13rc2/src/playground/memory/__init__.py +0 -14
- universal_mcp-0.1.13rc2/src/playground/memory/sqlite.py +0 -9
- {universal_mcp-0.1.13rc2 → universal_mcp-0.1.13rc7}/.gitignore +0 -0
- {universal_mcp-0.1.13rc2 → universal_mcp-0.1.13rc7}/README.md +0 -0
- {universal_mcp-0.1.13rc2 → universal_mcp-0.1.13rc7}/src/playground/README.md +0 -0
- {universal_mcp-0.1.13rc2 → universal_mcp-0.1.13rc7}/src/playground/__init__.py +0 -0
- {universal_mcp-0.1.13rc2 → universal_mcp-0.1.13rc7}/src/playground/schema.py +0 -0
- {universal_mcp-0.1.13rc2 → universal_mcp-0.1.13rc7}/src/playground/settings.py +0 -0
- {universal_mcp-0.1.13rc2 → universal_mcp-0.1.13rc7}/src/playground/utils.py +0 -0
- {universal_mcp-0.1.13rc2 → universal_mcp-0.1.13rc7}/src/tests/__init__.py +0 -0
- {universal_mcp-0.1.13rc2 → universal_mcp-0.1.13rc7}/src/tests/conftest.py +0 -0
- {universal_mcp-0.1.13rc2 → universal_mcp-0.1.13rc7}/src/tests/test_localserver.py +0 -0
- {universal_mcp-0.1.13rc2 → universal_mcp-0.1.13rc7}/src/tests/test_tool.py +0 -0
- {universal_mcp-0.1.13rc2 → universal_mcp-0.1.13rc7}/src/tests/test_zenquotes.py +0 -0
- {universal_mcp-0.1.13rc2 → universal_mcp-0.1.13rc7}/src/universal_mcp/__init__.py +0 -0
- {universal_mcp-0.1.13rc2 → universal_mcp-0.1.13rc7}/src/universal_mcp/analytics.py +0 -0
- {universal_mcp-0.1.13rc2 → universal_mcp-0.1.13rc7}/src/universal_mcp/applications/application.py +0 -0
- {universal_mcp-0.1.13rc2 → universal_mcp-0.1.13rc7}/src/universal_mcp/config.py +0 -0
- {universal_mcp-0.1.13rc2 → universal_mcp-0.1.13rc7}/src/universal_mcp/exceptions.py +0 -0
- {universal_mcp-0.1.13rc2 → universal_mcp-0.1.13rc7}/src/universal_mcp/integrations/README.md +0 -0
- {universal_mcp-0.1.13rc2 → universal_mcp-0.1.13rc7}/src/universal_mcp/integrations/__init__.py +1 -1
- {universal_mcp-0.1.13rc2 → universal_mcp-0.1.13rc7}/src/universal_mcp/integrations/integration.py +0 -0
- {universal_mcp-0.1.13rc2 → universal_mcp-0.1.13rc7}/src/universal_mcp/logger.py +0 -0
- {universal_mcp-0.1.13rc2 → universal_mcp-0.1.13rc7}/src/universal_mcp/py.typed +0 -0
- {universal_mcp-0.1.13rc2 → universal_mcp-0.1.13rc7}/src/universal_mcp/servers/README.md +0 -0
- {universal_mcp-0.1.13rc2 → universal_mcp-0.1.13rc7}/src/universal_mcp/servers/__init__.py +0 -0
- {universal_mcp-0.1.13rc2 → universal_mcp-0.1.13rc7}/src/universal_mcp/stores/README.md +0 -0
- {universal_mcp-0.1.13rc2 → universal_mcp-0.1.13rc7}/src/universal_mcp/stores/__init__.py +0 -0
- {universal_mcp-0.1.13rc2 → universal_mcp-0.1.13rc7}/src/universal_mcp/templates/README.md.j2 +0 -0
- {universal_mcp-0.1.13rc2 → universal_mcp-0.1.13rc7}/src/universal_mcp/templates/api_client.py.j2 +0 -0
- {universal_mcp-0.1.13rc2 → universal_mcp-0.1.13rc7}/src/universal_mcp/tools/README.md +0 -0
- {universal_mcp-0.1.13rc2 → universal_mcp-0.1.13rc7}/src/universal_mcp/tools/__init__.py +0 -0
- {universal_mcp-0.1.13rc2 → universal_mcp-0.1.13rc7}/src/universal_mcp/tools/adapters.py +0 -0
- {universal_mcp-0.1.13rc2 → universal_mcp-0.1.13rc7}/src/universal_mcp/tools/func_metadata.py +0 -0
- {universal_mcp-0.1.13rc2 → universal_mcp-0.1.13rc7}/src/universal_mcp/utils/__init__.py +0 -0
- {universal_mcp-0.1.13rc2 → universal_mcp-0.1.13rc7}/src/universal_mcp/utils/docgen.py +0 -0
- {universal_mcp-0.1.13rc2 → universal_mcp-0.1.13rc7}/src/universal_mcp/utils/docstring_parser.py +0 -0
- {universal_mcp-0.1.13rc2 → universal_mcp-0.1.13rc7}/src/universal_mcp/utils/dump_app_tools.py +0 -0
- {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.
|
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-
|
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(
|
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
|
@@ -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
|
-
""",
|
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
|
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(
|
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
|
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(
|
332
|
+
st.markdown("</div>", unsafe_allow_html=True)
|
326
333
|
|
327
334
|
# End chat app wrapper
|
328
|
-
st.markdown(
|
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"
|
80
|
-
assert "readme_file"
|
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
|
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:
|
{universal_mcp-0.1.13rc2 → universal_mcp-0.1.13rc7}/src/universal_mcp/applications/__init__.py
RENAMED
@@ -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(
|
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(
|
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(
|
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(
|
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(
|
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",
|