universal-mcp 0.1.13rc3__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.13rc3 → universal_mcp-0.1.13rc7}/PKG-INFO +1 -1
- {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc7}/pyproject.toml +1 -41
- {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc7}/src/playground/__main__.py +3 -1
- {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc7}/src/playground/client.py +40 -41
- {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc7}/src/playground/streamlit.py +21 -10
- {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc7}/src/tests/test_api_generator.py +3 -4
- {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc7}/src/tests/test_api_integration.py +2 -2
- {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc7}/src/tests/test_applications.py +1 -1
- {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc7}/src/tests/test_stores.py +1 -0
- {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc7}/src/universal_mcp/applications/__init__.py +20 -6
- {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc7}/src/universal_mcp/cli.py +14 -16
- {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc7}/src/universal_mcp/servers/server.py +0 -2
- {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc7}/src/universal_mcp/stores/store.py +2 -0
- {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc7}/src/universal_mcp/tools/tools.py +1 -3
- {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc7}/src/universal_mcp/utils/agentr.py +8 -3
- {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc7}/src/universal_mcp/utils/api_generator.py +26 -23
- {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc7}/src/universal_mcp/utils/openapi.py +54 -38
- 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 → universal_mcp-0.1.13rc7}/.gitignore +0 -0
- {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc7}/README.md +0 -0
- {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc7}/src/playground/README.md +0 -0
- {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc7}/src/playground/__init__.py +0 -0
- {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc7}/src/playground/agents/react.py +0 -0
- {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc7}/src/playground/schema.py +0 -0
- {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc7}/src/playground/settings.py +0 -0
- {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc7}/src/playground/utils.py +0 -0
- {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc7}/src/tests/__init__.py +0 -0
- {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc7}/src/tests/conftest.py +0 -0
- {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc7}/src/tests/test_localserver.py +0 -0
- {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc7}/src/tests/test_tool.py +0 -0
- {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc7}/src/tests/test_zenquotes.py +0 -0
- {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc7}/src/universal_mcp/__init__.py +0 -0
- {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc7}/src/universal_mcp/analytics.py +0 -0
- {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc7}/src/universal_mcp/applications/application.py +0 -0
- {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc7}/src/universal_mcp/config.py +0 -0
- {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc7}/src/universal_mcp/exceptions.py +0 -0
- {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc7}/src/universal_mcp/integrations/README.md +0 -0
- {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc7}/src/universal_mcp/integrations/__init__.py +1 -1
- {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc7}/src/universal_mcp/integrations/integration.py +0 -0
- {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc7}/src/universal_mcp/logger.py +0 -0
- {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc7}/src/universal_mcp/py.typed +0 -0
- {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc7}/src/universal_mcp/servers/README.md +0 -0
- {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc7}/src/universal_mcp/servers/__init__.py +0 -0
- {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc7}/src/universal_mcp/stores/README.md +0 -0
- {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc7}/src/universal_mcp/stores/__init__.py +0 -0
- {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc7}/src/universal_mcp/templates/README.md.j2 +0 -0
- {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc7}/src/universal_mcp/templates/api_client.py.j2 +0 -0
- {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc7}/src/universal_mcp/tools/README.md +0 -0
- {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc7}/src/universal_mcp/tools/__init__.py +0 -0
- {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc7}/src/universal_mcp/tools/adapters.py +0 -0
- {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc7}/src/universal_mcp/tools/func_metadata.py +0 -0
- {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc7}/src/universal_mcp/utils/__init__.py +0 -0
- {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc7}/src/universal_mcp/utils/docgen.py +0 -0
- {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc7}/src/universal_mcp/utils/docstring_parser.py +0 -0
- {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc7}/src/universal_mcp/utils/dump_app_tools.py +0 -0
- {universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc7}/src/universal_mcp/utils/installation.py +0 -0
- {universal_mcp-0.1.13rc3 → 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
|
@@ -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 = [
|
@@ -199,43 +199,3 @@ docstring-code-line-length = "dynamic"
|
|
199
199
|
[tool.pytest.ini_options]
|
200
200
|
asyncio_mode = "strict"
|
201
201
|
asyncio_default_fixture_loop_scope = "function"
|
202
|
-
|
203
|
-
[tool.uv.sources]
|
204
|
-
universal-mcp-ahrefs = { git = "https://github.com/universal-mcp/ahrefs" }
|
205
|
-
universal-mcp-cal-com-v2 = { git = "https://github.com/universal-mcp/cal-com-v2" }
|
206
|
-
universal-mcp-calendly = { git = "https://github.com/universal-mcp/calendly" }
|
207
|
-
universal-mcp-clickup = { git = "https://github.com/universal-mcp/clickup" }
|
208
|
-
universal-mcp-coda = { git = "https://github.com/universal-mcp/coda" }
|
209
|
-
universal-mcp-crustdata = { git = "https://github.com/universal-mcp/crustdata" }
|
210
|
-
universal-mcp-e2b = { git = "https://github.com/universal-mcp/e2b" }
|
211
|
-
universal-mcp-elevenlabs = { git = "https://github.com/universal-mcp/elevenlabs" }
|
212
|
-
universal-mcp-falai = { git = "https://github.com/universal-mcp/falai" }
|
213
|
-
universal-mcp-figma = { git = "https://github.com/universal-mcp/figma" }
|
214
|
-
universal-mcp-firecrawl = { git = "https://github.com/universal-mcp/firecrawl" }
|
215
|
-
universal-mcp-github = { git = "https://github.com/universal-mcp/github" }
|
216
|
-
universal-mcp-gong = { git = "https://github.com/universal-mcp/gong" }
|
217
|
-
universal-mcp-google-calendar = { git = "https://github.com/universal-mcp/google-calendar" }
|
218
|
-
universal-mcp-google-docs = { git = "https://github.com/universal-mcp/google-docs" }
|
219
|
-
universal-mcp-google-drive = { git = "https://github.com/universal-mcp/google-drive" }
|
220
|
-
universal-mcp-google-mail = { git = "https://github.com/universal-mcp/google-mail" }
|
221
|
-
universal-mcp-google-sheet = { git = "https://github.com/universal-mcp/google-sheet" }
|
222
|
-
universal-mcp-hashnode = { git = "https://github.com/universal-mcp/hashnode" }
|
223
|
-
universal-mcp-heygen = { git = "https://github.com/universal-mcp/heygen" }
|
224
|
-
universal-mcp-mailchimp = { git = "https://github.com/universal-mcp/mailchimp" }
|
225
|
-
universal-mcp-markitdown = { git = "https://github.com/universal-mcp/markitdown" }
|
226
|
-
universal-mcp-neon = { git = "https://github.com/universal-mcp/neon" }
|
227
|
-
universal-mcp-notion = { git = "https://github.com/universal-mcp/notion" }
|
228
|
-
universal-mcp-perplexity = { git = "https://github.com/universal-mcp/perplexity" }
|
229
|
-
universal-mcp-reddit = { git = "https://github.com/universal-mcp/reddit" }
|
230
|
-
universal-mcp-replicate = { git = "https://github.com/universal-mcp/replicate" }
|
231
|
-
universal-mcp-resend = { git = "https://github.com/universal-mcp/resend" }
|
232
|
-
universal-mcp-retell = { git = "https://github.com/universal-mcp/retell" }
|
233
|
-
universal-mcp-rocketlane = { git = "https://github.com/universal-mcp/rocketlane" }
|
234
|
-
universal-mcp-serpapi = { git = "https://github.com/universal-mcp/serpapi" }
|
235
|
-
universal-mcp-shortcut = { git = "https://github.com/universal-mcp/shortcut" }
|
236
|
-
universal-mcp-spotify = { git = "https://github.com/universal-mcp/spotify" }
|
237
|
-
universal-mcp-supabase = { git = "https://github.com/universal-mcp/supabase" }
|
238
|
-
universal-mcp-tavily = { git = "https://github.com/universal-mcp/tavily" }
|
239
|
-
universal-mcp-wrike = { git = "https://github.com/universal-mcp/wrike" }
|
240
|
-
universal-mcp-youtube = { git = "https://github.com/universal-mcp/youtube" }
|
241
|
-
universal-mcp-zenquotes = { git = "https://github.com/universal-mcp/zenquotes" }
|
@@ -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,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.13rc3 → 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",
|
@@ -1,11 +1,9 @@
|
|
1
|
-
import
|
2
|
-
import os
|
1
|
+
import re
|
3
2
|
from pathlib import Path
|
4
3
|
|
5
4
|
import typer
|
6
5
|
from rich import print as rprint
|
7
6
|
from rich.panel import Panel
|
8
|
-
import re
|
9
7
|
|
10
8
|
from universal_mcp.utils.installation import (
|
11
9
|
get_supported_apps,
|
@@ -41,9 +39,9 @@ def generate(
|
|
41
39
|
try:
|
42
40
|
# Run the async function in the event loop
|
43
41
|
result = generate_api_from_schema(
|
44
|
-
|
45
|
-
|
46
|
-
|
42
|
+
schema_path=schema_path,
|
43
|
+
output_path=output_path,
|
44
|
+
)
|
47
45
|
|
48
46
|
if not output_path:
|
49
47
|
# Print to stdout if no output path
|
@@ -68,7 +66,6 @@ def docgen(
|
|
68
66
|
"-m",
|
69
67
|
help="Model to use for generating docstrings",
|
70
68
|
),
|
71
|
-
|
72
69
|
):
|
73
70
|
"""Generate docstrings for Python files using LLMs.
|
74
71
|
|
@@ -154,6 +151,7 @@ def install(app_name: str = typer.Argument(..., help="Name of app to install")):
|
|
154
151
|
typer.echo(f"Error installing app: {e}", err=True)
|
155
152
|
raise typer.Exit(1) from e
|
156
153
|
|
154
|
+
|
157
155
|
@app.command()
|
158
156
|
def init(
|
159
157
|
output_dir: Path | None = typer.Option(
|
@@ -162,13 +160,13 @@ def init(
|
|
162
160
|
"-o",
|
163
161
|
help="Output directory for the project (must exist)",
|
164
162
|
),
|
165
|
-
app_name: str|None = typer.Option(
|
163
|
+
app_name: str | None = typer.Option(
|
166
164
|
None,
|
167
165
|
"--app-name",
|
168
166
|
"-a",
|
169
167
|
help="App name (letters, numbers, hyphens, underscores only)",
|
170
168
|
),
|
171
|
-
integration_type: str|None = typer.Option(
|
169
|
+
integration_type: str | None = typer.Option(
|
172
170
|
None,
|
173
171
|
"--integration-type",
|
174
172
|
"-i",
|
@@ -195,7 +193,7 @@ def init(
|
|
195
193
|
app_name = typer.prompt(
|
196
194
|
"Enter the app name",
|
197
195
|
default="app_name",
|
198
|
-
prompt_suffix=" (e.g., reddit, youtube): "
|
196
|
+
prompt_suffix=" (e.g., reddit, youtube): ",
|
199
197
|
).strip()
|
200
198
|
validate_pattern(app_name, "app name")
|
201
199
|
|
@@ -203,10 +201,10 @@ def init(
|
|
203
201
|
path_str = typer.prompt(
|
204
202
|
"Enter the output directory for the project",
|
205
203
|
default=str(Path.cwd()),
|
206
|
-
prompt_suffix=": "
|
204
|
+
prompt_suffix=": ",
|
207
205
|
).strip()
|
208
206
|
output_dir = Path(path_str)
|
209
|
-
|
207
|
+
|
210
208
|
if not output_dir.exists():
|
211
209
|
try:
|
212
210
|
output_dir.mkdir(parents=True, exist_ok=True)
|
@@ -219,7 +217,7 @@ def init(
|
|
219
217
|
f"❌ Failed to create output directory '{output_dir}': {e}",
|
220
218
|
fg=typer.colors.RED,
|
221
219
|
)
|
222
|
-
raise typer.Exit(code=1)
|
220
|
+
raise typer.Exit(code=1) from e
|
223
221
|
elif not output_dir.is_dir():
|
224
222
|
typer.secho(
|
225
223
|
f"❌ Output path '{output_dir}' exists but is not a directory.",
|
@@ -232,7 +230,7 @@ def init(
|
|
232
230
|
integration_type = typer.prompt(
|
233
231
|
"Choose the integration type",
|
234
232
|
default="agentr",
|
235
|
-
prompt_suffix=" (api_key, oauth, agentr, none): "
|
233
|
+
prompt_suffix=" (api_key, oauth, agentr, none): ",
|
236
234
|
).lower()
|
237
235
|
if integration_type not in ("api_key", "oauth", "agentr", "none"):
|
238
236
|
typer.secho(
|
@@ -240,7 +238,6 @@ def init(
|
|
240
238
|
fg=typer.colors.RED,
|
241
239
|
)
|
242
240
|
raise typer.Exit(code=1)
|
243
|
-
|
244
241
|
|
245
242
|
typer.secho("🚀 Generating project using cookiecutter...", fg=typer.colors.BLUE)
|
246
243
|
try:
|
@@ -255,10 +252,11 @@ def init(
|
|
255
252
|
)
|
256
253
|
except Exception as exc:
|
257
254
|
typer.secho(f"❌ Project generation failed: {exc}", fg=typer.colors.RED)
|
258
|
-
raise typer.Exit(code=1)
|
255
|
+
raise typer.Exit(code=1) from exc
|
259
256
|
|
260
257
|
project_dir = output_dir / f"universal-mcp-{app_name}"
|
261
258
|
typer.secho(f"✅ Project created at {project_dir}", fg=typer.colors.GREEN)
|
262
259
|
|
260
|
+
|
263
261
|
if __name__ == "__main__":
|
264
262
|
app()
|
@@ -11,11 +11,13 @@ class StoreError(Exception):
|
|
11
11
|
|
12
12
|
pass
|
13
13
|
|
14
|
+
|
14
15
|
class KeyNotFoundError(StoreError):
|
15
16
|
"""Exception raised when a key is not found in the store."""
|
16
17
|
|
17
18
|
pass
|
18
19
|
|
20
|
+
|
19
21
|
class BaseStore(ABC):
|
20
22
|
"""
|
21
23
|
Abstract base class defining the interface for credential stores.
|
@@ -260,9 +260,7 @@ class ToolManager:
|
|
260
260
|
try:
|
261
261
|
available_tool_functions = app.list_tools()
|
262
262
|
except TypeError as e:
|
263
|
-
logger.error(
|
264
|
-
f"Error calling list_tools for app '{app.name}'. Error: {e}"
|
265
|
-
)
|
263
|
+
logger.error(f"Error calling list_tools for app '{app.name}'. Error: {e}")
|
266
264
|
return
|
267
265
|
except Exception as e:
|
268
266
|
logger.error(f"Failed to get tool list from app '{app.name}': {e}")
|
@@ -1,9 +1,13 @@
|
|
1
|
-
from loguru import logger
|
2
1
|
import os
|
2
|
+
|
3
3
|
import httpx
|
4
|
+
from loguru import logger
|
5
|
+
|
4
6
|
from universal_mcp.config import AppConfig
|
7
|
+
from universal_mcp.exceptions import NotAuthorizedError
|
5
8
|
from universal_mcp.utils.singleton import Singleton
|
6
9
|
|
10
|
+
|
7
11
|
class AgentrClient(metaclass=Singleton):
|
8
12
|
"""Helper class for AgentR API operations.
|
9
13
|
|
@@ -22,7 +26,9 @@ class AgentrClient(metaclass=Singleton):
|
|
22
26
|
"API key for AgentR is missing. Please visit https://agentr.dev to create an API key, then set it as AGENTR_API_KEY environment variable."
|
23
27
|
)
|
24
28
|
raise ValueError("AgentR API key required - get one at https://agentr.dev")
|
25
|
-
self.base_url = (
|
29
|
+
self.base_url = (
|
30
|
+
base_url or os.getenv("AGENTR_BASE_URL", "https://api.agentr.dev")
|
31
|
+
).rstrip("/")
|
26
32
|
|
27
33
|
def get_credentials(self, integration_name: str) -> dict:
|
28
34
|
"""Get credentials for an integration from the AgentR API.
|
@@ -87,4 +93,3 @@ class AgentrClient(metaclass=Singleton):
|
|
87
93
|
response.raise_for_status()
|
88
94
|
data = response.json()
|
89
95
|
return [AppConfig.model_validate(app) for app in data]
|
90
|
-
|
{universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc7}/src/universal_mcp/utils/api_generator.py
RENAMED
@@ -1,10 +1,11 @@
|
|
1
|
+
import importlib.util
|
1
2
|
import inspect
|
2
3
|
import os
|
3
|
-
from pathlib import Path
|
4
|
-
from loguru import logger
|
5
4
|
import shutil
|
6
|
-
import
|
5
|
+
from pathlib import Path
|
6
|
+
|
7
7
|
from jinja2 import Environment, FileSystemLoader, TemplateError, select_autoescape
|
8
|
+
from loguru import logger
|
8
9
|
|
9
10
|
from universal_mcp.utils.openapi import generate_api_client, load_schema
|
10
11
|
|
@@ -26,6 +27,7 @@ def validate_and_load_schema(schema_path: Path) -> dict:
|
|
26
27
|
echo(f"Error loading schema: {e}", err=True)
|
27
28
|
raise
|
28
29
|
|
30
|
+
|
29
31
|
def get_class_info(module: any) -> tuple[str | None, any]:
|
30
32
|
"""Find the main class in the generated module."""
|
31
33
|
for name, obj in inspect.getmembers(module):
|
@@ -33,19 +35,18 @@ def get_class_info(module: any) -> tuple[str | None, any]:
|
|
33
35
|
return name, obj
|
34
36
|
return None, None
|
35
37
|
|
36
|
-
|
37
|
-
|
38
|
-
) -> Path:
|
38
|
+
|
39
|
+
def generate_readme(app_dir: Path, folder_name: str, tools: list) -> Path:
|
39
40
|
"""Generate README.md with API documentation.
|
40
|
-
|
41
|
+
|
41
42
|
Args:
|
42
43
|
app_dir: Directory where the README will be generated
|
43
44
|
folder_name: Name of the application folder
|
44
45
|
tools: List of Function objects from the OpenAPI schema
|
45
|
-
|
46
|
+
|
46
47
|
Returns:
|
47
48
|
Path to the generated README file
|
48
|
-
|
49
|
+
|
49
50
|
Raises:
|
50
51
|
FileNotFoundError: If the template directory doesn't exist
|
51
52
|
TemplateError: If there's an error rendering the template
|
@@ -69,23 +70,19 @@ def generate_readme(
|
|
69
70
|
|
70
71
|
try:
|
71
72
|
env = Environment(
|
72
|
-
loader=FileSystemLoader(template_dir),
|
73
|
-
autoescape=select_autoescape()
|
73
|
+
loader=FileSystemLoader(template_dir), autoescape=select_autoescape()
|
74
74
|
)
|
75
75
|
template = env.get_template("README.md.j2")
|
76
76
|
except Exception as e:
|
77
77
|
logger.error(f"Error loading template: {e}")
|
78
|
-
raise TemplateError(f"Error loading template: {e}")
|
78
|
+
raise TemplateError(f"Error loading template: {e}") from e
|
79
79
|
|
80
80
|
# Render the template
|
81
81
|
try:
|
82
|
-
readme_content = template.render(
|
83
|
-
name=app,
|
84
|
-
tools=formatted_tools
|
85
|
-
)
|
82
|
+
readme_content = template.render(name=app, tools=formatted_tools)
|
86
83
|
except Exception as e:
|
87
84
|
logger.error(f"Error rendering template: {e}")
|
88
|
-
raise TemplateError(f"Error rendering template: {e}")
|
85
|
+
raise TemplateError(f"Error rendering template: {e}") from e
|
89
86
|
|
90
87
|
# Write the README file
|
91
88
|
readme_file = app_dir / "README.md"
|
@@ -95,10 +92,11 @@ def generate_readme(
|
|
95
92
|
logger.info(f"Documentation generated at: {readme_file}")
|
96
93
|
except Exception as e:
|
97
94
|
logger.error(f"Error writing README file: {e}")
|
98
|
-
raise
|
95
|
+
raise OSError(f"Error writing README file: {e}") from e
|
99
96
|
|
100
97
|
return readme_file
|
101
98
|
|
99
|
+
|
102
100
|
def test_correct_output(gen_file: Path):
|
103
101
|
# Check file is non-empty
|
104
102
|
if gen_file.stat().st_size == 0:
|
@@ -137,7 +135,6 @@ def generate_api_from_schema(
|
|
137
135
|
"""
|
138
136
|
# Local imports for logging and file operations
|
139
137
|
|
140
|
-
|
141
138
|
logger.info("Starting API generation for schema: %s", schema_path)
|
142
139
|
|
143
140
|
# 1. Parse and validate schema
|
@@ -174,10 +171,15 @@ def generate_api_from_schema(
|
|
174
171
|
f.write(code)
|
175
172
|
|
176
173
|
if not test_correct_output(gen_file):
|
177
|
-
logger.error(
|
174
|
+
logger.error(
|
175
|
+
"Generated code validation failed for '%s'. Aborting generation.", gen_file
|
176
|
+
)
|
178
177
|
logger.info("Next steps:")
|
179
178
|
logger.info(" 1) Review your OpenAPI schema for potential mismatches.")
|
180
|
-
logger.info(
|
179
|
+
logger.info(
|
180
|
+
" 2) Inspect '%s' for syntax or logic errors in the generated code.",
|
181
|
+
gen_file,
|
182
|
+
)
|
181
183
|
logger.info(" 3) Correct the issues and re-run the command.")
|
182
184
|
return {"error": "Validation failed. See logs above for detailed instructions."}
|
183
185
|
|
@@ -210,13 +212,14 @@ def generate_api_from_schema(
|
|
210
212
|
client = cls()
|
211
213
|
tools = client.list_tools()
|
212
214
|
except Exception as e:
|
213
|
-
logger.warning(
|
215
|
+
logger.warning(
|
216
|
+
"Failed to instantiate '%s' or list tools: %s", class_name, e
|
217
|
+
)
|
214
218
|
else:
|
215
219
|
logger.warning("No generated class found in module 'temp_module'")
|
216
220
|
readme_file = generate_readme(target_dir, output_path.stem, tools)
|
217
221
|
logger.info("README generated at: %s", readme_file)
|
218
222
|
|
219
|
-
|
220
223
|
# Cleanup intermediate file
|
221
224
|
try:
|
222
225
|
os.remove(gen_file)
|
@@ -1,15 +1,16 @@
|
|
1
1
|
import json
|
2
2
|
import re
|
3
|
+
from dataclasses import dataclass
|
3
4
|
from pathlib import Path
|
4
|
-
from typing import Any,
|
5
|
-
from loguru import logger
|
5
|
+
from typing import Any, Literal
|
6
6
|
|
7
7
|
import yaml
|
8
8
|
from jinja2 import Environment, FileSystemLoader, select_autoescape
|
9
|
-
from
|
9
|
+
from loguru import logger
|
10
|
+
|
10
11
|
|
11
12
|
def convert_to_snake_case(identifier: str) -> str:
|
12
|
-
"""
|
13
|
+
"""
|
13
14
|
Convert a camelCase or PascalCase identifier to snake_case.
|
14
15
|
|
15
16
|
Args:
|
@@ -140,15 +141,13 @@ def generate_api_client(schema):
|
|
140
141
|
# Set up Jinja2 environment
|
141
142
|
env = Environment(
|
142
143
|
loader=FileSystemLoader(Path(__file__).parent.parent / "templates"),
|
143
|
-
autoescape=select_autoescape()
|
144
|
+
autoescape=select_autoescape(),
|
144
145
|
)
|
145
146
|
template = env.get_template("api_client.py.j2")
|
146
147
|
|
147
148
|
# Render the template
|
148
149
|
class_code = template.render(
|
149
|
-
class_name=class_name,
|
150
|
-
base_url=base_url,
|
151
|
-
methods=methods
|
150
|
+
class_name=class_name, base_url=base_url, methods=methods
|
152
151
|
)
|
153
152
|
|
154
153
|
return class_code
|
@@ -158,10 +157,10 @@ def generate_api_client(schema):
|
|
158
157
|
class Function:
|
159
158
|
name: str
|
160
159
|
type: Literal["get", "post", "put", "delete", "patch", "options", "head"]
|
161
|
-
args:
|
160
|
+
args: dict[str, str]
|
162
161
|
return_type: str
|
163
162
|
description: str
|
164
|
-
tags:
|
163
|
+
tags: list[str]
|
165
164
|
implementation: str
|
166
165
|
|
167
166
|
@property
|
@@ -171,10 +170,7 @@ class Function:
|
|
171
170
|
|
172
171
|
|
173
172
|
def generate_method_code(
|
174
|
-
path: str,
|
175
|
-
method: str,
|
176
|
-
operation: dict[str, Any],
|
177
|
-
full_schema: dict[str, Any]
|
173
|
+
path: str, method: str, operation: dict[str, Any], full_schema: dict[str, Any]
|
178
174
|
) -> Function:
|
179
175
|
"""
|
180
176
|
Generate a Function object for a single API method.
|
@@ -205,7 +201,6 @@ def generate_method_code(
|
|
205
201
|
return "dict[str, Any]"
|
206
202
|
return "Any"
|
207
203
|
|
208
|
-
|
209
204
|
# Determine function name
|
210
205
|
if op_id := operation.get("operationId"):
|
211
206
|
cleaned_id = op_id.replace(".", "_").replace("-", "_")
|
@@ -240,10 +235,12 @@ def generate_method_code(
|
|
240
235
|
# Analyze requestBody
|
241
236
|
has_body = "requestBody" in operation
|
242
237
|
body_required = bool(has_body and operation["requestBody"].get("required"))
|
243
|
-
content = (
|
238
|
+
content = (
|
239
|
+
(operation.get("requestBody", {}) or {}).get("content", {}) if has_body else {}
|
240
|
+
)
|
244
241
|
is_array_body = False
|
245
|
-
request_props:
|
246
|
-
required_fields:
|
242
|
+
request_props: dict[str, Any] = {}
|
243
|
+
required_fields: list[str] = []
|
247
244
|
if has_body and content:
|
248
245
|
for mime, info in content.items():
|
249
246
|
if not mime.startswith("application/json") or "schema" not in info:
|
@@ -258,10 +255,12 @@ def generate_method_code(
|
|
258
255
|
request_props = schema.get("properties", {}) or {}
|
259
256
|
for name, prop_schema in list(request_props.items()):
|
260
257
|
if pre := prop_schema.get("$ref"):
|
261
|
-
request_props[name] =
|
258
|
+
request_props[name] = (
|
259
|
+
resolve_schema_reference(pre, full_schema) or prop_schema
|
260
|
+
)
|
262
261
|
|
263
262
|
# Build function arguments with Annotated[type, description]
|
264
|
-
arg_defs:
|
263
|
+
arg_defs: dict[str, str] = {}
|
265
264
|
for p in path_params:
|
266
265
|
name = p["name"]
|
267
266
|
ty = map_type(p.get("schema", {}))
|
@@ -304,7 +303,7 @@ def generate_method_code(
|
|
304
303
|
# Assemble description
|
305
304
|
summary = operation.get("summary", "")
|
306
305
|
operation_desc = operation.get("description", "")
|
307
|
-
desc_parts:
|
306
|
+
desc_parts: list[str] = []
|
308
307
|
if summary:
|
309
308
|
desc_parts.append(summary)
|
310
309
|
if operation_desc:
|
@@ -315,25 +314,33 @@ def generate_method_code(
|
|
315
314
|
|
316
315
|
# Generate implementation code
|
317
316
|
implementation_lines = []
|
318
|
-
|
317
|
+
|
319
318
|
# Add parameter validation for required fields
|
320
319
|
for param in path_params + query_params:
|
321
320
|
if param.get("required"):
|
322
321
|
name = param["name"]
|
323
322
|
implementation_lines.append(f"if {name} is None:")
|
324
|
-
implementation_lines.append(
|
325
|
-
|
323
|
+
implementation_lines.append(
|
324
|
+
f" raise ValueError(\"Missing required parameter '{name}'\")"
|
325
|
+
)
|
326
|
+
|
326
327
|
if has_body and body_required:
|
327
328
|
if is_array_body:
|
328
329
|
implementation_lines.append("if items is None:")
|
329
|
-
implementation_lines.append(
|
330
|
+
implementation_lines.append(
|
331
|
+
" raise ValueError(\"Missing required parameter 'items'\")"
|
332
|
+
)
|
330
333
|
elif request_props:
|
331
334
|
for prop in required_fields:
|
332
335
|
implementation_lines.append(f"if {prop} is None:")
|
333
|
-
implementation_lines.append(
|
336
|
+
implementation_lines.append(
|
337
|
+
f" raise ValueError(\"Missing required parameter '{prop}'\")"
|
338
|
+
)
|
334
339
|
else:
|
335
340
|
implementation_lines.append("if request_body is None:")
|
336
|
-
implementation_lines.append(
|
341
|
+
implementation_lines.append(
|
342
|
+
" raise ValueError(\"Missing required parameter 'request_body'\")"
|
343
|
+
)
|
337
344
|
|
338
345
|
# Build request body
|
339
346
|
if has_body:
|
@@ -342,35 +349,43 @@ def generate_method_code(
|
|
342
349
|
elif request_props:
|
343
350
|
implementation_lines.append("request_body = {")
|
344
351
|
for prop in request_props:
|
345
|
-
implementation_lines.append(f
|
352
|
+
implementation_lines.append(f' "{prop}": {prop},')
|
346
353
|
implementation_lines.append("}")
|
347
|
-
implementation_lines.append(
|
354
|
+
implementation_lines.append(
|
355
|
+
"request_body = {k: v for k, v in request_body.items() if v is not None}"
|
356
|
+
)
|
348
357
|
else:
|
349
358
|
implementation_lines.append("request_body = request_body")
|
350
359
|
|
351
360
|
# Build URL with path parameters
|
352
|
-
path = "/".join([path_params["name"] for path_params in path_params]) or '
|
353
|
-
url = '
|
354
|
-
implementation_lines.append(f
|
355
|
-
implementation_lines.append(f
|
361
|
+
path = "/".join([path_params["name"] for path_params in path_params]) or '""'
|
362
|
+
url = '"{self.base_url}{path}"'
|
363
|
+
implementation_lines.append(f"path = {path}")
|
364
|
+
implementation_lines.append(f"url = f{url}")
|
356
365
|
|
357
366
|
# Build query parameters
|
358
367
|
if query_params:
|
359
368
|
implementation_lines.append("query_params = {")
|
360
369
|
for param in query_params:
|
361
370
|
name = param["name"]
|
362
|
-
implementation_lines.append(f
|
371
|
+
implementation_lines.append(f' "{name}": {name},')
|
363
372
|
implementation_lines.append(" }")
|
364
|
-
implementation_lines.append(
|
373
|
+
implementation_lines.append(
|
374
|
+
"query_params = {k: v for k, v in query_params.items() if v is not None}"
|
375
|
+
)
|
365
376
|
else:
|
366
377
|
implementation_lines.append("query_params = {}")
|
367
378
|
|
368
379
|
# Make the request using the appropriate method
|
369
380
|
http_method = method.lower()
|
370
381
|
if has_body:
|
371
|
-
implementation_lines.append(
|
382
|
+
implementation_lines.append(
|
383
|
+
f"response = self._{http_method}(url, data=request_body, params=query_params)"
|
384
|
+
)
|
372
385
|
else:
|
373
|
-
implementation_lines.append(
|
386
|
+
implementation_lines.append(
|
387
|
+
f"response = self._{http_method}(url, params=query_params)"
|
388
|
+
)
|
374
389
|
|
375
390
|
# Handle response
|
376
391
|
implementation_lines.append("response.raise_for_status()")
|
@@ -386,12 +401,13 @@ def generate_method_code(
|
|
386
401
|
return_type=return_type,
|
387
402
|
description=description_text,
|
388
403
|
tags=tags,
|
389
|
-
implementation=implementation
|
404
|
+
implementation=implementation,
|
390
405
|
)
|
391
406
|
|
392
407
|
logger.debug(f"Generated function: {function}")
|
393
408
|
return function
|
394
409
|
|
410
|
+
|
395
411
|
# Example usage
|
396
412
|
if __name__ == "__main__":
|
397
413
|
# Sample OpenAPI schema
|
@@ -1,14 +0,0 @@
|
|
1
|
-
from langgraph.checkpoint.base import BaseCheckpointSaver
|
2
|
-
|
3
|
-
from playground.memory.sqlite import get_sqlite_saver
|
4
|
-
|
5
|
-
|
6
|
-
def initialize_database() -> BaseCheckpointSaver:
|
7
|
-
"""
|
8
|
-
Initialize the appropriate database checkpointer based on configuration.
|
9
|
-
Returns an initialized AsyncCheckpointer instance.
|
10
|
-
"""
|
11
|
-
return get_sqlite_saver()
|
12
|
-
|
13
|
-
|
14
|
-
__all__ = ["initialize_database"]
|
@@ -1,9 +0,0 @@
|
|
1
|
-
from langgraph.checkpoint.base import BaseCheckpointSaver
|
2
|
-
from langgraph.checkpoint.sqlite.aio import AsyncSqliteSaver
|
3
|
-
|
4
|
-
from playground.settings import settings
|
5
|
-
|
6
|
-
|
7
|
-
def get_sqlite_saver() -> BaseCheckpointSaver:
|
8
|
-
"""Initialize and return a SQLite saver instance."""
|
9
|
-
return AsyncSqliteSaver.from_conn_string(settings.SQLITE_DB_PATH)
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
{universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc7}/src/universal_mcp/applications/application.py
RENAMED
File without changes
|
File without changes
|
File without changes
|
{universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc7}/src/universal_mcp/integrations/README.md
RENAMED
File without changes
|
{universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc7}/src/universal_mcp/integrations/integration.py
RENAMED
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
{universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc7}/src/universal_mcp/templates/README.md.j2
RENAMED
File without changes
|
{universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc7}/src/universal_mcp/templates/api_client.py.j2
RENAMED
File without changes
|
File without changes
|
File without changes
|
File without changes
|
{universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc7}/src/universal_mcp/tools/func_metadata.py
RENAMED
File without changes
|
File without changes
|
File without changes
|
{universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc7}/src/universal_mcp/utils/docstring_parser.py
RENAMED
File without changes
|
{universal_mcp-0.1.13rc3 → universal_mcp-0.1.13rc7}/src/universal_mcp/utils/dump_app_tools.py
RENAMED
File without changes
|
File without changes
|
File without changes
|