universal-mcp 0.1.23rc1__py3-none-any.whl → 0.1.23rc2__py3-none-any.whl
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/applications/application.py +0 -107
- universal_mcp/cli.py +2 -1
- universal_mcp/client/__main__.py +30 -0
- universal_mcp/client/agent.py +96 -0
- universal_mcp/client/client.py +198 -0
- universal_mcp/client/oauth.py +114 -0
- universal_mcp/client/token_store.py +32 -0
- universal_mcp/config.py +62 -3
- universal_mcp/integrations/integration.py +2 -2
- universal_mcp/servers/server.py +5 -5
- universal_mcp/tools/adapters.py +16 -0
- universal_mcp/utils/openapi/api_splitter.py +9 -19
- universal_mcp/utils/openapi/openapi.py +15 -11
- {universal_mcp-0.1.23rc1.dist-info → universal_mcp-0.1.23rc2.dist-info}/METADATA +2 -2
- {universal_mcp-0.1.23rc1.dist-info → universal_mcp-0.1.23rc2.dist-info}/RECORD +18 -13
- {universal_mcp-0.1.23rc1.dist-info → universal_mcp-0.1.23rc2.dist-info}/WHEEL +0 -0
- {universal_mcp-0.1.23rc1.dist-info → universal_mcp-0.1.23rc2.dist-info}/entry_points.txt +0 -0
- {universal_mcp-0.1.23rc1.dist-info → universal_mcp-0.1.23rc2.dist-info}/licenses/LICENSE +0 -0
@@ -374,113 +374,6 @@ class APIApplication(BaseApplication):
|
|
374
374
|
logger.debug(f"PATCH request successful with status code: {response.status_code}")
|
375
375
|
return response
|
376
376
|
|
377
|
-
# New convenience methods that handle responses automatically with enhanced error handling
|
378
|
-
def _get_json(self, url: str, params: dict[str, Any] | None = None) -> dict[str, Any] | str:
|
379
|
-
"""
|
380
|
-
Make a GET request and automatically handle the response with enhanced error handling.
|
381
|
-
|
382
|
-
Args:
|
383
|
-
url: The URL to send the request to
|
384
|
-
params: Optional query parameters
|
385
|
-
|
386
|
-
Returns:
|
387
|
-
dict[str, Any] | str: Parsed JSON response if available, otherwise success message
|
388
|
-
|
389
|
-
Raises:
|
390
|
-
httpx.HTTPStatusError: If the request fails with detailed error information including response body
|
391
|
-
"""
|
392
|
-
response = self._get(url, params)
|
393
|
-
return self._handle_response(response)
|
394
|
-
|
395
|
-
def _post_json(
|
396
|
-
self,
|
397
|
-
url: str,
|
398
|
-
data: Any,
|
399
|
-
params: dict[str, Any] | None = None,
|
400
|
-
content_type: str = "application/json",
|
401
|
-
files: dict[str, Any] | None = None,
|
402
|
-
) -> dict[str, Any] | str:
|
403
|
-
"""
|
404
|
-
Make a POST request and automatically handle the response with enhanced error handling.
|
405
|
-
|
406
|
-
Args:
|
407
|
-
url: The URL to send the request to
|
408
|
-
data: The data to send
|
409
|
-
params: Optional query parameters
|
410
|
-
content_type: The Content-Type of the request body
|
411
|
-
files: Optional dictionary of files to upload
|
412
|
-
|
413
|
-
Returns:
|
414
|
-
dict[str, Any] | str: Parsed JSON response if available, otherwise success message
|
415
|
-
|
416
|
-
Raises:
|
417
|
-
httpx.HTTPStatusError: If the request fails with detailed error information including response body
|
418
|
-
"""
|
419
|
-
response = self._post(url, data, params, content_type, files)
|
420
|
-
return self._handle_response(response)
|
421
|
-
|
422
|
-
def _put_json(
|
423
|
-
self,
|
424
|
-
url: str,
|
425
|
-
data: Any,
|
426
|
-
params: dict[str, Any] | None = None,
|
427
|
-
content_type: str = "application/json",
|
428
|
-
files: dict[str, Any] | None = None,
|
429
|
-
) -> dict[str, Any] | str:
|
430
|
-
"""
|
431
|
-
Make a PUT request and automatically handle the response with enhanced error handling.
|
432
|
-
|
433
|
-
Args:
|
434
|
-
url: The URL to send the request to
|
435
|
-
data: The data to send
|
436
|
-
params: Optional query parameters
|
437
|
-
content_type: The Content-Type of the request body
|
438
|
-
files: Optional dictionary of files to upload
|
439
|
-
|
440
|
-
Returns:
|
441
|
-
dict[str, Any] | str: Parsed JSON response if available, otherwise success message
|
442
|
-
|
443
|
-
Raises:
|
444
|
-
httpx.HTTPStatusError: If the request fails with detailed error information including response body
|
445
|
-
"""
|
446
|
-
response = self._put(url, data, params, content_type, files)
|
447
|
-
return self._handle_response(response)
|
448
|
-
|
449
|
-
def _delete_json(self, url: str, params: dict[str, Any] | None = None) -> dict[str, Any] | str:
|
450
|
-
"""
|
451
|
-
Make a DELETE request and automatically handle the response with enhanced error handling.
|
452
|
-
|
453
|
-
Args:
|
454
|
-
url: The URL to send the request to
|
455
|
-
params: Optional query parameters
|
456
|
-
|
457
|
-
Returns:
|
458
|
-
dict[str, Any] | str: Parsed JSON response if available, otherwise success message
|
459
|
-
|
460
|
-
Raises:
|
461
|
-
httpx.HTTPStatusError: If the request fails with detailed error information including response body
|
462
|
-
"""
|
463
|
-
response = self._delete(url, params)
|
464
|
-
return self._handle_response(response)
|
465
|
-
|
466
|
-
def _patch_json(self, url: str, data: dict[str, Any], params: dict[str, Any] | None = None) -> dict[str, Any] | str:
|
467
|
-
"""
|
468
|
-
Make a PATCH request and automatically handle the response with enhanced error handling.
|
469
|
-
|
470
|
-
Args:
|
471
|
-
url: The URL to send the request to
|
472
|
-
data: The data to send in the request body
|
473
|
-
params: Optional query parameters
|
474
|
-
|
475
|
-
Returns:
|
476
|
-
dict[str, Any] | str: Parsed JSON response if available, otherwise success message
|
477
|
-
|
478
|
-
Raises:
|
479
|
-
httpx.HTTPStatusError: If the request fails with detailed error information including response body
|
480
|
-
"""
|
481
|
-
response = self._patch(url, data, params)
|
482
|
-
return self._handle_response(response)
|
483
|
-
|
484
377
|
|
485
378
|
class GraphQLApplication(BaseApplication):
|
486
379
|
"""
|
universal_mcp/cli.py
CHANGED
@@ -270,6 +270,7 @@ def preprocess(
|
|
270
270
|
def split_api(
|
271
271
|
input_app_file: Path = typer.Argument(..., help="Path to the generated app.py file to split"),
|
272
272
|
output_dir: Path = typer.Option(..., "--output-dir", "-o", help="Directory to save the split files"),
|
273
|
+
package_name: str = typer.Option(None, "--package-name", "-p", help="Package name for absolute imports (e.g., 'hubspot')"),
|
273
274
|
):
|
274
275
|
"""Splits a single generated API client file into multiple files based on path groups."""
|
275
276
|
from universal_mcp.utils.openapi.api_splitter import split_generated_app_file
|
@@ -286,7 +287,7 @@ def split_api(
|
|
286
287
|
raise typer.Exit(1)
|
287
288
|
|
288
289
|
try:
|
289
|
-
split_generated_app_file(input_app_file, output_dir)
|
290
|
+
split_generated_app_file(input_app_file, output_dir, package_name)
|
290
291
|
console.print(f"[green]Successfully split {input_app_file} into {output_dir}[/green]")
|
291
292
|
except Exception as e:
|
292
293
|
console.print(f"[red]Error splitting API client: {e}[/red]")
|
@@ -0,0 +1,30 @@
|
|
1
|
+
import asyncio
|
2
|
+
import os
|
3
|
+
import sys
|
4
|
+
|
5
|
+
from loguru import logger
|
6
|
+
from pydantic import ValidationError
|
7
|
+
|
8
|
+
from universal_mcp.client.agent import ChatSession
|
9
|
+
from universal_mcp.client.client import MultiClientServer
|
10
|
+
from universal_mcp.config import ClientConfig
|
11
|
+
|
12
|
+
|
13
|
+
async def main() -> None:
|
14
|
+
"""Initialize and run the chat session."""
|
15
|
+
# Load settings and config using Pydantic BaseSettings
|
16
|
+
|
17
|
+
config_path = os.getenv("MCP_CONFIG_PATH", "servers.json")
|
18
|
+
try:
|
19
|
+
app_config = ClientConfig.load_json_config(config_path)
|
20
|
+
except (FileNotFoundError, ValidationError) as e:
|
21
|
+
logger.error(f"Error loading config: {e}")
|
22
|
+
sys.exit(1)
|
23
|
+
|
24
|
+
async with MultiClientServer(app_config.mcpServers) as mcp_server:
|
25
|
+
chat_session = ChatSession(mcp_server, app_config.llm)
|
26
|
+
await chat_session.interactive_loop()
|
27
|
+
|
28
|
+
|
29
|
+
if __name__ == "__main__":
|
30
|
+
asyncio.run(main())
|
@@ -0,0 +1,96 @@
|
|
1
|
+
import json
|
2
|
+
|
3
|
+
from loguru import logger
|
4
|
+
from mcp.server import Server as MCPServer
|
5
|
+
from openai import AsyncOpenAI
|
6
|
+
|
7
|
+
from universal_mcp.config import LLMConfig
|
8
|
+
|
9
|
+
|
10
|
+
class ChatSession:
|
11
|
+
"""Orchestrates the interaction between user, LLM, and tools."""
|
12
|
+
|
13
|
+
def __init__(self, mcp_server: MCPServer, llm: LLMConfig | None) -> None:
|
14
|
+
self.mcp_server: MCPServer = mcp_server
|
15
|
+
self.llm: AsyncOpenAI | None = AsyncOpenAI(api_key=llm.api_key, base_url=llm.base_url) if llm else None
|
16
|
+
self.model = llm.model if llm else None
|
17
|
+
|
18
|
+
async def run(self, messages, tools) -> None:
|
19
|
+
"""Run the chat session."""
|
20
|
+
llm_response = await self.llm.chat.completions.create(
|
21
|
+
model=self.model,
|
22
|
+
messages=messages,
|
23
|
+
tools=tools,
|
24
|
+
tool_choice="auto",
|
25
|
+
)
|
26
|
+
|
27
|
+
tool_calls = llm_response.choices[0].message.tool_calls
|
28
|
+
if tool_calls:
|
29
|
+
for tool_call in tool_calls:
|
30
|
+
result = await self.mcp_server.call_tool(
|
31
|
+
tool_name=tool_call.function.name,
|
32
|
+
arguments=json.loads(tool_call.function.arguments) if tool_call.function.arguments else {},
|
33
|
+
)
|
34
|
+
result_content = [rc.text for rc in result.content] if result.content else "No result"
|
35
|
+
messages.append(
|
36
|
+
{
|
37
|
+
"tool_call_id": tool_call.id,
|
38
|
+
"role": "tool",
|
39
|
+
"name": tool_call.function.name,
|
40
|
+
"content": result_content,
|
41
|
+
}
|
42
|
+
)
|
43
|
+
else:
|
44
|
+
messages.append(llm_response.choices[0].message)
|
45
|
+
return messages
|
46
|
+
|
47
|
+
async def interactive_loop(self) -> None:
|
48
|
+
"""Main chat session handler."""
|
49
|
+
all_openai_tools = await self.mcp_server.list_tools(format="openai")
|
50
|
+
system_message = "You are a helpful assistant"
|
51
|
+
messages = [{"role": "system", "content": system_message}]
|
52
|
+
|
53
|
+
print("\n🎯 Interactive MCP Client")
|
54
|
+
print("Commands:")
|
55
|
+
print(" list - List available tools")
|
56
|
+
print(" call <tool_name> [args] - Call a tool")
|
57
|
+
print(" quit - Exit the client")
|
58
|
+
print()
|
59
|
+
while True:
|
60
|
+
try:
|
61
|
+
user_input = input("You: ").strip()
|
62
|
+
if user_input.lower() in {"quit", "exit"}:
|
63
|
+
logger.info("\nExiting...")
|
64
|
+
break
|
65
|
+
elif user_input.lower() == "list":
|
66
|
+
tools = await self.mcp_server.list_tools()
|
67
|
+
print("\nAvailable tools:")
|
68
|
+
for tool in tools:
|
69
|
+
print(f" {tool.name}")
|
70
|
+
continue
|
71
|
+
elif user_input.startswith("call "):
|
72
|
+
parts = user_input.split(maxsplit=2)
|
73
|
+
tool_name = parts[1] if len(parts) > 1 else ""
|
74
|
+
|
75
|
+
if not tool_name:
|
76
|
+
print("❌ Please specify a tool name")
|
77
|
+
continue
|
78
|
+
|
79
|
+
# Parse arguments (simple JSON-like format)
|
80
|
+
arguments = {}
|
81
|
+
if len(parts) > 2:
|
82
|
+
try:
|
83
|
+
arguments = json.loads(parts[2])
|
84
|
+
except json.JSONDecodeError:
|
85
|
+
print("❌ Invalid arguments format (expected JSON)")
|
86
|
+
continue
|
87
|
+
await self.mcp_server.call_tool(tool_name, arguments)
|
88
|
+
|
89
|
+
messages.append({"role": "user", "content": user_input})
|
90
|
+
|
91
|
+
messages = await self.run(messages, all_openai_tools)
|
92
|
+
print("\nAssistant: ", messages[-1]["content"])
|
93
|
+
|
94
|
+
except KeyboardInterrupt:
|
95
|
+
print("\nExiting...")
|
96
|
+
break
|
@@ -0,0 +1,198 @@
|
|
1
|
+
import os
|
2
|
+
import webbrowser
|
3
|
+
from contextlib import AsyncExitStack
|
4
|
+
from typing import Any, Literal
|
5
|
+
|
6
|
+
from loguru import logger
|
7
|
+
from mcp import ClientSession, StdioServerParameters
|
8
|
+
from mcp.client.auth import OAuthClientProvider
|
9
|
+
from mcp.client.sse import sse_client
|
10
|
+
from mcp.client.stdio import stdio_client
|
11
|
+
from mcp.client.streamable_http import streamablehttp_client
|
12
|
+
from mcp.server import Server
|
13
|
+
from mcp.shared.auth import OAuthClientMetadata
|
14
|
+
from mcp.types import (
|
15
|
+
CallToolResult as MCPCallToolResult,
|
16
|
+
)
|
17
|
+
from mcp.types import (
|
18
|
+
Tool as MCPTool,
|
19
|
+
)
|
20
|
+
from openai.types.chat import ChatCompletionToolParam
|
21
|
+
|
22
|
+
from universal_mcp.client.oauth import CallbackServer
|
23
|
+
from universal_mcp.client.token_store import TokenStore
|
24
|
+
from universal_mcp.config import ClientTransportConfig
|
25
|
+
from universal_mcp.stores.store import KeyringStore
|
26
|
+
from universal_mcp.tools.adapters import transform_mcp_tool_to_openai_tool
|
27
|
+
|
28
|
+
|
29
|
+
class MCPClient:
|
30
|
+
"""Manages MCP server connections and tool execution."""
|
31
|
+
|
32
|
+
def __init__(self, name: str, config: ClientTransportConfig) -> None:
|
33
|
+
self.name: str = name
|
34
|
+
self.config: ClientTransportConfig = config
|
35
|
+
self.session: ClientSession | None = None
|
36
|
+
self.server_url: str = config.url
|
37
|
+
|
38
|
+
# Set up callback server
|
39
|
+
self.callback_server = CallbackServer(port=3000)
|
40
|
+
self.callback_server.start()
|
41
|
+
|
42
|
+
# Create OAuth authentication handler using the new interface
|
43
|
+
if self.server_url and not self.config.headers:
|
44
|
+
self.store = KeyringStore(self.name)
|
45
|
+
self.auth = OAuthClientProvider(
|
46
|
+
server_url="/".join(self.server_url.split("/")[:-1]),
|
47
|
+
client_metadata=OAuthClientMetadata.model_validate(self.client_metadata_dict),
|
48
|
+
storage=TokenStore(self.store),
|
49
|
+
redirect_handler=self._default_redirect_handler,
|
50
|
+
callback_handler=self._callback_handler,
|
51
|
+
)
|
52
|
+
else:
|
53
|
+
self.auth = None
|
54
|
+
|
55
|
+
async def _callback_handler(self) -> tuple[str, str | None]:
|
56
|
+
"""Wait for OAuth callback and return auth code and state."""
|
57
|
+
print("⏳ Waiting for authorization callback...")
|
58
|
+
try:
|
59
|
+
auth_code = self.callback_server.wait_for_callback(timeout=300)
|
60
|
+
return auth_code, self.callback_server.get_state()
|
61
|
+
finally:
|
62
|
+
self.callback_server.stop()
|
63
|
+
|
64
|
+
@property
|
65
|
+
def client_metadata_dict(self) -> dict[str, Any]:
|
66
|
+
return {
|
67
|
+
"client_name": "Simple Auth Client",
|
68
|
+
"redirect_uris": ["http://localhost:3000/callback"],
|
69
|
+
"grant_types": ["authorization_code", "refresh_token"],
|
70
|
+
"response_types": ["code"],
|
71
|
+
"token_endpoint_auth_method": "client_secret_post",
|
72
|
+
}
|
73
|
+
|
74
|
+
async def _default_redirect_handler(self, authorization_url: str) -> None:
|
75
|
+
"""Default redirect handler that opens the URL in a browser."""
|
76
|
+
print(f"Opening browser for authorization: {authorization_url}")
|
77
|
+
webbrowser.open(authorization_url)
|
78
|
+
|
79
|
+
async def initialize(self, exit_stack: AsyncExitStack):
|
80
|
+
"""Initialize the server connection."""
|
81
|
+
transport = self.config.transport
|
82
|
+
try:
|
83
|
+
if transport == "stdio":
|
84
|
+
command = self.config["command"]
|
85
|
+
if command is None:
|
86
|
+
raise ValueError("The command must be a valid string and cannot be None.")
|
87
|
+
|
88
|
+
server_params = StdioServerParameters(
|
89
|
+
command=command,
|
90
|
+
args=self.config["args"],
|
91
|
+
env={**os.environ, **self.config["env"]} if self.config.get("env") else None,
|
92
|
+
)
|
93
|
+
stdio_transport = await exit_stack.enter_async_context(stdio_client(server_params))
|
94
|
+
read, write = stdio_transport
|
95
|
+
session = await exit_stack.enter_async_context(ClientSession(read, write))
|
96
|
+
await session.initialize()
|
97
|
+
self.session = session
|
98
|
+
elif transport == "streamable_http":
|
99
|
+
url = self.config.get("url")
|
100
|
+
headers = self.config.get("headers", {})
|
101
|
+
if not url:
|
102
|
+
raise ValueError("'url' must be provided for streamable_http transport.")
|
103
|
+
streamable_http_transport = await exit_stack.enter_async_context(
|
104
|
+
streamablehttp_client(url=url, headers=headers, auth=self.auth)
|
105
|
+
)
|
106
|
+
read, write, _ = streamable_http_transport
|
107
|
+
session = await exit_stack.enter_async_context(ClientSession(read, write))
|
108
|
+
await session.initialize()
|
109
|
+
self.session = session
|
110
|
+
elif transport == "sse":
|
111
|
+
url = self.config.url
|
112
|
+
headers = self.config.headers
|
113
|
+
if not url:
|
114
|
+
raise ValueError("'url' must be provided for sse transport.")
|
115
|
+
sse_transport = await exit_stack.enter_async_context(
|
116
|
+
sse_client(url=url, headers=headers, auth=self.auth)
|
117
|
+
)
|
118
|
+
read, write = sse_transport
|
119
|
+
session = await exit_stack.enter_async_context(ClientSession(read, write))
|
120
|
+
await session.initialize()
|
121
|
+
self.session = session
|
122
|
+
else:
|
123
|
+
raise ValueError(f"Unknown transport: {transport}")
|
124
|
+
except Exception as e:
|
125
|
+
logger.error(f"Error initializing server {self.name}: {e}")
|
126
|
+
raise
|
127
|
+
|
128
|
+
async def list_tools(self) -> list[MCPTool]:
|
129
|
+
"""List available tools from the server."""
|
130
|
+
if self.session:
|
131
|
+
tools = await self.session.list_tools()
|
132
|
+
return list(tools.tools)
|
133
|
+
return []
|
134
|
+
|
135
|
+
async def call_tool(self, tool_name: str, arguments: dict[str, Any]) -> MCPCallToolResult:
|
136
|
+
"""Call a tool on the server."""
|
137
|
+
if self.session:
|
138
|
+
return await self.session.call_tool(tool_name, arguments)
|
139
|
+
return MCPCallToolResult(
|
140
|
+
content=[],
|
141
|
+
isError=True,
|
142
|
+
)
|
143
|
+
|
144
|
+
|
145
|
+
class MultiClientServer(Server):
|
146
|
+
"""
|
147
|
+
Manages multiple MCP servers and maintains a mapping from tool name to the server that provides it.
|
148
|
+
"""
|
149
|
+
|
150
|
+
def __init__(self, clients: dict[str, ClientTransportConfig]):
|
151
|
+
self.clients: list[MCPClient] = [MCPClient(name, config) for name, config in clients.items()]
|
152
|
+
self.tool_to_client: dict[str, MCPClient] = {}
|
153
|
+
self._mcp_tools: list[MCPTool] = []
|
154
|
+
self._exit_stack: AsyncExitStack = AsyncExitStack()
|
155
|
+
|
156
|
+
async def __aenter__(self):
|
157
|
+
"""Initialize the server connection."""
|
158
|
+
for client in self.clients:
|
159
|
+
await client.initialize(self._exit_stack)
|
160
|
+
await self._populate_tool_mapping()
|
161
|
+
return self
|
162
|
+
|
163
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
164
|
+
"""Clean up the server connection."""
|
165
|
+
self.clients.clear()
|
166
|
+
self.tool_to_client.clear()
|
167
|
+
self._mcp_tools.clear()
|
168
|
+
await self._exit_stack.aclose()
|
169
|
+
|
170
|
+
async def _populate_tool_mapping(self):
|
171
|
+
"""Populate the mapping from tool name to server."""
|
172
|
+
self.tool_to_client.clear()
|
173
|
+
self._mcp_tools.clear()
|
174
|
+
for client in self.clients:
|
175
|
+
try:
|
176
|
+
tools = await client.list_tools()
|
177
|
+
for tool in tools:
|
178
|
+
self._mcp_tools.append(tool)
|
179
|
+
tool_name = tool.name
|
180
|
+
logger.info(f"Found tool: {tool_name} from client: {client.name}")
|
181
|
+
if tool_name:
|
182
|
+
self.tool_to_client[tool_name] = client
|
183
|
+
except Exception as e:
|
184
|
+
logger.warning(f"Failed to list tools for client {client.name}: {e}")
|
185
|
+
|
186
|
+
async def list_tools(self, format: Literal["mcp", "openai"] = "mcp") -> list[MCPTool | ChatCompletionToolParam]:
|
187
|
+
"""List available tools from all servers."""
|
188
|
+
if format == "mcp":
|
189
|
+
return self._mcp_tools
|
190
|
+
elif format == "openai":
|
191
|
+
return [transform_mcp_tool_to_openai_tool(tool) for tool in self._mcp_tools]
|
192
|
+
else:
|
193
|
+
raise ValueError(f"Invalid format: {format}")
|
194
|
+
|
195
|
+
async def call_tool(self, tool_name: str, arguments: dict[str, Any]) -> MCPCallToolResult:
|
196
|
+
"""Call a tool on the server."""
|
197
|
+
client = self.tool_to_client[tool_name]
|
198
|
+
return await client.call_tool(tool_name, arguments)
|
@@ -0,0 +1,114 @@
|
|
1
|
+
import threading
|
2
|
+
import time
|
3
|
+
from http.server import BaseHTTPRequestHandler, HTTPServer
|
4
|
+
from urllib.parse import parse_qs, urlparse
|
5
|
+
|
6
|
+
from universal_mcp.utils.singleton import Singleton
|
7
|
+
|
8
|
+
|
9
|
+
class CallbackHandler(BaseHTTPRequestHandler):
|
10
|
+
"""Simple HTTP handler to capture OAuth callback."""
|
11
|
+
|
12
|
+
def __init__(self, request, client_address, server, callback_data):
|
13
|
+
"""Initialize with callback data storage."""
|
14
|
+
self.callback_data = callback_data
|
15
|
+
super().__init__(request, client_address, server)
|
16
|
+
|
17
|
+
def do_GET(self):
|
18
|
+
"""Handle GET request from OAuth redirect."""
|
19
|
+
parsed = urlparse(self.path)
|
20
|
+
query_params = parse_qs(parsed.query)
|
21
|
+
|
22
|
+
if "code" in query_params:
|
23
|
+
self.callback_data["authorization_code"] = query_params["code"][0]
|
24
|
+
self.callback_data["state"] = query_params.get("state", [None])[0]
|
25
|
+
self.send_response(200)
|
26
|
+
self.send_header("Content-type", "text/html")
|
27
|
+
self.end_headers()
|
28
|
+
self.wfile.write(b"""
|
29
|
+
<html>
|
30
|
+
<body>
|
31
|
+
<h1>Authorization Successful!</h1>
|
32
|
+
<p>You can close this window and return to the terminal.</p>
|
33
|
+
<script>setTimeout(() => window.close(), 2000);</script>
|
34
|
+
</body>
|
35
|
+
</html>
|
36
|
+
""")
|
37
|
+
elif "error" in query_params:
|
38
|
+
self.callback_data["error"] = query_params["error"][0]
|
39
|
+
self.send_response(400)
|
40
|
+
self.send_header("Content-type", "text/html")
|
41
|
+
self.end_headers()
|
42
|
+
self.wfile.write(
|
43
|
+
f"""
|
44
|
+
<html>
|
45
|
+
<body>
|
46
|
+
<h1>Authorization Failed</h1>
|
47
|
+
<p>Error: {query_params['error'][0]}</p>
|
48
|
+
<p>You can close this window and return to the terminal.</p>
|
49
|
+
</body>
|
50
|
+
</html>
|
51
|
+
""".encode()
|
52
|
+
)
|
53
|
+
else:
|
54
|
+
self.send_response(404)
|
55
|
+
self.end_headers()
|
56
|
+
|
57
|
+
def log_message(self, format, *args):
|
58
|
+
"""Suppress default logging."""
|
59
|
+
pass
|
60
|
+
|
61
|
+
|
62
|
+
class CallbackServer(metaclass=Singleton):
|
63
|
+
"""Simple server to handle OAuth callbacks."""
|
64
|
+
|
65
|
+
def __init__(self, port=3000):
|
66
|
+
self.port = port
|
67
|
+
self.server = None
|
68
|
+
self.thread = None
|
69
|
+
self.callback_data = {"authorization_code": None, "state": None, "error": None}
|
70
|
+
self._running = False
|
71
|
+
|
72
|
+
def _create_handler_with_data(self):
|
73
|
+
"""Create a handler class with access to callback data."""
|
74
|
+
callback_data = self.callback_data
|
75
|
+
|
76
|
+
class DataCallbackHandler(CallbackHandler):
|
77
|
+
def __init__(self, request, client_address, server):
|
78
|
+
super().__init__(request, client_address, server, callback_data)
|
79
|
+
|
80
|
+
return DataCallbackHandler
|
81
|
+
|
82
|
+
def start(self):
|
83
|
+
"""Start the callback server in a background thread."""
|
84
|
+
if self._running:
|
85
|
+
return
|
86
|
+
handler_class = self._create_handler_with_data()
|
87
|
+
self.server = HTTPServer(("localhost", self.port), handler_class)
|
88
|
+
self.thread = threading.Thread(target=self.server.serve_forever, daemon=True)
|
89
|
+
self.thread.start()
|
90
|
+
print(f"🖥️ Started callback server on http://localhost:{self.port}")
|
91
|
+
self._running = True
|
92
|
+
|
93
|
+
def stop(self):
|
94
|
+
"""Stop the callback server."""
|
95
|
+
if self.server:
|
96
|
+
self.server.shutdown()
|
97
|
+
self.server.server_close()
|
98
|
+
if self.thread:
|
99
|
+
self.thread.join(timeout=1)
|
100
|
+
|
101
|
+
def wait_for_callback(self, timeout=300):
|
102
|
+
"""Wait for OAuth callback with timeout."""
|
103
|
+
start_time = time.time()
|
104
|
+
while time.time() - start_time < timeout:
|
105
|
+
if self.callback_data["authorization_code"]:
|
106
|
+
return self.callback_data["authorization_code"]
|
107
|
+
elif self.callback_data["error"]:
|
108
|
+
raise Exception(f"OAuth error: {self.callback_data['error']}")
|
109
|
+
time.sleep(0.1)
|
110
|
+
raise Exception("Timeout waiting for OAuth callback")
|
111
|
+
|
112
|
+
def get_state(self):
|
113
|
+
"""Get the received state parameter."""
|
114
|
+
return self.callback_data["state"]
|
@@ -0,0 +1,32 @@
|
|
1
|
+
from mcp.client.auth import TokenStorage as MCPTokenStorage
|
2
|
+
from mcp.shared.auth import OAuthClientInformationFull, OAuthToken
|
3
|
+
|
4
|
+
from universal_mcp.exceptions import KeyNotFoundError
|
5
|
+
from universal_mcp.stores.store import KeyringStore
|
6
|
+
|
7
|
+
|
8
|
+
class TokenStore(MCPTokenStorage):
|
9
|
+
"""Simple in-memory token storage implementation."""
|
10
|
+
|
11
|
+
def __init__(self, store: KeyringStore):
|
12
|
+
self.store = store
|
13
|
+
self._tokens: OAuthToken | None = None
|
14
|
+
self._client_info: OAuthClientInformationFull | None = None
|
15
|
+
|
16
|
+
async def get_tokens(self) -> OAuthToken | None:
|
17
|
+
try:
|
18
|
+
return OAuthToken.model_validate_json(self.store.get("tokens"))
|
19
|
+
except KeyNotFoundError:
|
20
|
+
return None
|
21
|
+
|
22
|
+
async def set_tokens(self, tokens: OAuthToken) -> None:
|
23
|
+
self.store.set("tokens", tokens.model_dump_json())
|
24
|
+
|
25
|
+
async def get_client_info(self) -> OAuthClientInformationFull | None:
|
26
|
+
try:
|
27
|
+
return OAuthClientInformationFull.model_validate_json(self.store.get("client_info"))
|
28
|
+
except KeyNotFoundError:
|
29
|
+
return None
|
30
|
+
|
31
|
+
async def set_client_info(self, client_info: OAuthClientInformationFull) -> None:
|
32
|
+
self.store.set("client_info", client_info.model_dump_json())
|
universal_mcp/config.py
CHANGED
@@ -1,7 +1,8 @@
|
|
1
|
+
import json
|
1
2
|
from pathlib import Path
|
2
|
-
from typing import Any, Literal
|
3
|
+
from typing import Any, Literal, Self
|
3
4
|
|
4
|
-
from pydantic import BaseModel, Field, SecretStr, field_validator
|
5
|
+
from pydantic import BaseModel, Field, SecretStr, field_validator, model_validator
|
5
6
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
6
7
|
|
7
8
|
|
@@ -19,7 +20,7 @@ class IntegrationConfig(BaseModel):
|
|
19
20
|
"""Configuration for API integrations."""
|
20
21
|
|
21
22
|
name: str = Field(..., description="Name of the integration")
|
22
|
-
type: Literal["api_key", "oauth", "agentr", "oauth2"] = Field(
|
23
|
+
type: Literal["api_key", "oauth", "agentr", "oauth2", "basic_auth"] = Field(
|
23
24
|
default="api_key", description="Type of authentication to use"
|
24
25
|
)
|
25
26
|
credentials: dict[str, Any] | None = Field(default=None, description="Integration-specific credentials")
|
@@ -46,6 +47,9 @@ class ServerConfig(BaseSettings):
|
|
46
47
|
|
47
48
|
name: str = Field(default="Universal MCP", description="Name of the MCP server")
|
48
49
|
description: str = Field(default="Universal MCP", description="Description of the MCP server")
|
50
|
+
base_url: str = Field(
|
51
|
+
default="https://api.agentr.dev", description="Base URL for AgentR API", alias="AGENTR_BASE_URL"
|
52
|
+
)
|
49
53
|
api_key: SecretStr | None = Field(default=None, description="API key for authentication", alias="AGENTR_API_KEY")
|
50
54
|
type: Literal["local", "agentr"] = Field(default="agentr", description="Type of server deployment")
|
51
55
|
transport: Literal["stdio", "sse", "streamable-http"] = Field(
|
@@ -70,3 +74,58 @@ class ServerConfig(BaseSettings):
|
|
70
74
|
if not 1 <= v <= 65535:
|
71
75
|
raise ValueError("Port must be between 1 and 65535")
|
72
76
|
return v
|
77
|
+
|
78
|
+
@classmethod
|
79
|
+
def load_json_config(cls, path: str = "local_config.json") -> Self:
|
80
|
+
with open(path) as f:
|
81
|
+
data = json.load(f)
|
82
|
+
return cls.model_validate(data)
|
83
|
+
|
84
|
+
|
85
|
+
class ClientTransportConfig(BaseModel):
|
86
|
+
transport: str | None = None
|
87
|
+
command: str | None = None
|
88
|
+
args: list[str] = []
|
89
|
+
env: dict[str, str] = {}
|
90
|
+
url: str | None = None
|
91
|
+
headers: dict[str, str] = {}
|
92
|
+
|
93
|
+
@model_validator(mode="after")
|
94
|
+
def model_validate(self) -> Self:
|
95
|
+
"""
|
96
|
+
Set the transport type based on the presence of command or url.
|
97
|
+
- If command is present, transport is 'stdio'.
|
98
|
+
- Else if url ends with 'mcp', transport is 'streamable_http'.
|
99
|
+
- Else, transport is 'sse'.
|
100
|
+
"""
|
101
|
+
if self.command:
|
102
|
+
self.transport = "stdio"
|
103
|
+
elif self.url:
|
104
|
+
# Remove search params from url
|
105
|
+
url = self.url.split("?")[0]
|
106
|
+
if url.rstrip("/").endswith("mcp"):
|
107
|
+
self.transport = "streamable_http"
|
108
|
+
elif url.rstrip("/").endswith("sse"):
|
109
|
+
self.transport = "sse"
|
110
|
+
else:
|
111
|
+
raise ValueError(f"Unknown transport: {self.url}")
|
112
|
+
else:
|
113
|
+
raise ValueError("Either command or url must be provided")
|
114
|
+
return self
|
115
|
+
|
116
|
+
|
117
|
+
class LLMConfig(BaseModel):
|
118
|
+
api_key: str
|
119
|
+
base_url: str
|
120
|
+
model: str
|
121
|
+
|
122
|
+
|
123
|
+
class ClientConfig(BaseSettings):
|
124
|
+
mcpServers: dict[str, ClientTransportConfig]
|
125
|
+
llm: LLMConfig | None = None
|
126
|
+
|
127
|
+
@classmethod
|
128
|
+
def load_json_config(cls, path: str = "servers.json") -> Self:
|
129
|
+
with open(path) as f:
|
130
|
+
data = json.load(f)
|
131
|
+
return cls.model_validate(data)
|
@@ -325,9 +325,9 @@ class AgentRIntegration(Integration):
|
|
325
325
|
ValueError: If no API key is provided or found in environment variables
|
326
326
|
"""
|
327
327
|
|
328
|
-
def __init__(self, name: str, api_key: str | None = None, **kwargs):
|
328
|
+
def __init__(self, name: str, api_key: str | None = None, base_url: str | None = None, **kwargs):
|
329
329
|
super().__init__(name, **kwargs)
|
330
|
-
self.client = AgentrClient(api_key=api_key)
|
330
|
+
self.client = AgentrClient(api_key=api_key, base_url=base_url)
|
331
331
|
self._credentials = None
|
332
332
|
|
333
333
|
def set_credentials(self, credentials: dict | None = None):
|
universal_mcp/servers/server.py
CHANGED
@@ -206,8 +206,8 @@ class AgentRServer(BaseServer):
|
|
206
206
|
self.api_key = config.api_key.get_secret_value() if config.api_key else None
|
207
207
|
if not self.api_key:
|
208
208
|
raise ValueError("API key is required for AgentR server")
|
209
|
-
logger.info(f"Initializing AgentR server with API key: {self.api_key}")
|
210
|
-
self.client = AgentrClient(api_key=self.api_key)
|
209
|
+
logger.info(f"Initializing AgentR server with API key: {self.api_key} and base URL: {config.base_url}")
|
210
|
+
self.client = AgentrClient(api_key=self.api_key, base_url=config.base_url)
|
211
211
|
self._load_apps()
|
212
212
|
|
213
213
|
def _fetch_apps(self) -> list[AppConfig]:
|
@@ -245,7 +245,7 @@ class AgentRServer(BaseServer):
|
|
245
245
|
"""
|
246
246
|
try:
|
247
247
|
integration = (
|
248
|
-
AgentRIntegration(name=app_config.integration.name, api_key=self.api_key)
|
248
|
+
AgentRIntegration(name=app_config.integration.name, api_key=self.api_key, base_url=self.config.base_url)
|
249
249
|
if app_config.integration
|
250
250
|
else None
|
251
251
|
)
|
@@ -276,10 +276,10 @@ class AgentRServer(BaseServer):
|
|
276
276
|
else:
|
277
277
|
logger.info(f"Successfully loaded {loaded_apps}/{len(app_configs)} apps from AgentR")
|
278
278
|
|
279
|
-
except Exception:
|
279
|
+
except Exception as e:
|
280
280
|
logger.error("Failed to load apps", exc_info=True)
|
281
281
|
# Don't raise the exception to allow server to start with partial functionality
|
282
|
-
logger.warning("Server will start with limited functionality due to app loading failures")
|
282
|
+
logger.warning(f"Server will start with limited functionality due to app loading failures: {e}")
|
283
283
|
|
284
284
|
|
285
285
|
class SingleMCPServer(BaseServer):
|
universal_mcp/tools/adapters.py
CHANGED
@@ -102,3 +102,19 @@ def convert_tool_to_openai_tool(
|
|
102
102
|
}
|
103
103
|
logger.debug(f"Successfully converted tool '{tool.name}' to OpenAI format")
|
104
104
|
return openai_tool
|
105
|
+
|
106
|
+
|
107
|
+
def transform_mcp_tool_to_openai_tool(mcp_tool: Tool):
|
108
|
+
"""Convert an MCP tool to an OpenAI tool."""
|
109
|
+
from openai.types import FunctionDefinition
|
110
|
+
from openai.types.chat import ChatCompletionToolParam
|
111
|
+
|
112
|
+
return ChatCompletionToolParam(
|
113
|
+
type="function",
|
114
|
+
function=FunctionDefinition(
|
115
|
+
name=mcp_tool.name,
|
116
|
+
description=mcp_tool.description or "",
|
117
|
+
parameters=mcp_tool.inputSchema,
|
118
|
+
strict=False,
|
119
|
+
),
|
120
|
+
)
|
@@ -15,11 +15,11 @@ class APISegmentBase:
|
|
15
15
|
def _get(self, url: str, params: dict = None, **kwargs):
|
16
16
|
return self.main_app_client._get(url, params=params, **kwargs)
|
17
17
|
|
18
|
-
def _post(self, url: str, data: Any = None,
|
19
|
-
return self.main_app_client._post(url, data=data,
|
18
|
+
def _post(self, url: str, data: Any = None, params: dict = None, content_type: str = None, files: Any = None, **kwargs):
|
19
|
+
return self.main_app_client._post(url, data=data, params=params, content_type=content_type, files=files, **kwargs)
|
20
20
|
|
21
|
-
def _put(self, url: str, data: Any = None,
|
22
|
-
return self.main_app_client._put(url, data=data,
|
21
|
+
def _put(self, url: str, data: Any = None, params: dict = None, content_type: str = None, files: Any = None, **kwargs):
|
22
|
+
return self.main_app_client._put(url, data=data, params=params, content_type=content_type, files=files, **kwargs)
|
23
23
|
|
24
24
|
def _patch(self, url: str, data: Any = None, params: dict = None, **kwargs):
|
25
25
|
return self.main_app_client._patch(url, data=data, params=params, **kwargs)
|
@@ -27,20 +27,9 @@ class APISegmentBase:
|
|
27
27
|
def _delete(self, url: str, params: dict = None, **kwargs):
|
28
28
|
return self.main_app_client._delete(url, params=params, **kwargs)
|
29
29
|
|
30
|
-
def
|
31
|
-
return self.main_app_client.
|
30
|
+
def _handle_response(self, response):
|
31
|
+
return self.main_app_client._handle_response(response)
|
32
32
|
|
33
|
-
def _post_json(self, url: str, data: Any = None, files: Any = None, params: dict = None, content_type: str = "application/json", **kwargs):
|
34
|
-
return self.main_app_client._post_json(url, data=data, files=files, params=params, content_type=content_type, **kwargs)
|
35
|
-
|
36
|
-
def _put_json(self, url: str, data: Any = None, files: Any = None, params: dict = None, content_type: str = "application/json", **kwargs):
|
37
|
-
return self.main_app_client._put_json(url, data=data, files=files, params=params, content_type=content_type, **kwargs)
|
38
|
-
|
39
|
-
def _patch_json(self, url: str, data: Any = None, params: dict = None, **kwargs):
|
40
|
-
return self.main_app_client._patch_json(url, data=data, params=params, **kwargs)
|
41
|
-
|
42
|
-
def _delete_json(self, url: str, params: dict = None, **kwargs):
|
43
|
-
return self.main_app_client._delete_json(url, params=params, **kwargs)
|
44
33
|
"""
|
45
34
|
|
46
35
|
|
@@ -165,7 +154,8 @@ class MethodTransformer(ast.NodeTransformer):
|
|
165
154
|
return self.generic_visit(node)
|
166
155
|
|
167
156
|
|
168
|
-
def split_generated_app_file(input_app_file: Path, output_dir: Path):
|
157
|
+
def split_generated_app_file(input_app_file: Path, output_dir: Path, package_name: str = None):
|
158
|
+
|
169
159
|
content = input_app_file.read_text()
|
170
160
|
tree = ast.parse(content)
|
171
161
|
|
@@ -515,7 +505,7 @@ def split_generated_app_file(input_app_file: Path, output_dir: Path):
|
|
515
505
|
# Adjust import path for segments subfolder
|
516
506
|
final_main_module_imports.append(
|
517
507
|
ast.ImportFrom(
|
518
|
-
module=f".{segments_foldername}.{seg_detail['module_name']}",
|
508
|
+
module=f"universal_mcp_{package_name}.{segments_foldername}.{seg_detail['module_name']}",
|
519
509
|
names=[ast.alias(name=seg_detail["class_name"])],
|
520
510
|
level=0,
|
521
511
|
)
|
@@ -150,7 +150,7 @@ def _sanitize_identifier(name: str | None) -> str:
|
|
150
150
|
|
151
151
|
# Initial replacements for common non-alphanumeric characters
|
152
152
|
sanitized = (
|
153
|
-
name.replace("-", "_").replace(".", "_").replace("[", "_").replace("]", "").replace("$", "_").replace("/", "_")
|
153
|
+
name.replace("-", "_").replace(".", "_").replace("[", "_").replace("]", "").replace("$", "_").replace("/", "_").replace("@", "at")
|
154
154
|
)
|
155
155
|
|
156
156
|
# Remove leading underscores, but preserve a single underscore if the name (after initial replace)
|
@@ -971,33 +971,37 @@ def _generate_method_code(path, method, operation):
|
|
971
971
|
# Use convenience methods that automatically handle responses and errors
|
972
972
|
|
973
973
|
if method_lower == "get":
|
974
|
-
body_lines.append("
|
974
|
+
body_lines.append(" response = self._get(url, params=query_params)")
|
975
|
+
body_lines.append(" return self._handle_response(response)")
|
975
976
|
elif method_lower == "post":
|
976
977
|
if selected_content_type == "multipart/form-data":
|
977
978
|
body_lines.append(
|
978
|
-
f"
|
979
|
+
f" response = self._post(url, data=request_body_data, files=files_data, params=query_params, content_type='{final_content_type_for_api_call}')"
|
979
980
|
)
|
980
981
|
else:
|
981
982
|
body_lines.append(
|
982
|
-
f"
|
983
|
+
f" response = self._post(url, data=request_body_data, params=query_params, content_type='{final_content_type_for_api_call}')"
|
983
984
|
)
|
985
|
+
body_lines.append(" return self._handle_response(response)")
|
984
986
|
elif method_lower == "put":
|
985
987
|
if selected_content_type == "multipart/form-data":
|
986
988
|
body_lines.append(
|
987
|
-
f"
|
989
|
+
f" response = self._put(url, data=request_body_data, files=files_data, params=query_params, content_type='{final_content_type_for_api_call}')"
|
988
990
|
)
|
989
991
|
else:
|
990
992
|
body_lines.append(
|
991
|
-
f"
|
993
|
+
f" response = self._put(url, data=request_body_data, params=query_params, content_type='{final_content_type_for_api_call}')"
|
992
994
|
)
|
995
|
+
body_lines.append(" return self._handle_response(response)")
|
993
996
|
elif method_lower == "patch":
|
994
|
-
body_lines.append("
|
997
|
+
body_lines.append(" response = self._patch(url, data=request_body_data, params=query_params)")
|
998
|
+
body_lines.append(" return self._handle_response(response)")
|
995
999
|
elif method_lower == "delete":
|
996
|
-
body_lines.append("
|
1000
|
+
body_lines.append(" response = self._delete(url, params=query_params)")
|
1001
|
+
body_lines.append(" return self._handle_response(response)")
|
997
1002
|
else:
|
998
|
-
body_lines.append(f"
|
999
|
-
|
1000
|
-
# No need for manual response handling anymore - convenience methods handle it automatically
|
1003
|
+
body_lines.append(f" response = self._{method_lower}(url, data=request_body_data, params=query_params)")
|
1004
|
+
body_lines.append(" return self._handle_response(response)")
|
1001
1005
|
|
1002
1006
|
# --- Combine Signature, Docstring, and Body for Final Method Code ---
|
1003
1007
|
method_code = signature + formatted_docstring + "\n" + "\n".join(body_lines)
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: universal-mcp
|
3
|
-
Version: 0.1.
|
3
|
+
Version: 0.1.23rc2
|
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
|
@@ -15,7 +15,7 @@ Requires-Dist: keyring>=25.6.0
|
|
15
15
|
Requires-Dist: langchain-mcp-adapters>=0.0.3
|
16
16
|
Requires-Dist: litellm>=1.30.7
|
17
17
|
Requires-Dist: loguru>=0.7.3
|
18
|
-
Requires-Dist: mcp>=1.9.
|
18
|
+
Requires-Dist: mcp>=1.9.3
|
19
19
|
Requires-Dist: posthog>=3.24.0
|
20
20
|
Requires-Dist: pydantic-settings>=2.8.1
|
21
21
|
Requires-Dist: pydantic>=2.11.1
|
@@ -1,25 +1,30 @@
|
|
1
1
|
universal_mcp/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
2
2
|
universal_mcp/analytics.py,sha256=Dkv8mkc_2T2t5NxLSZzcr3BlmOispj1RKtbB86V1i4M,2306
|
3
|
-
universal_mcp/cli.py,sha256
|
4
|
-
universal_mcp/config.py,sha256=
|
3
|
+
universal_mcp/cli.py,sha256=Ndc1I4BtmvDCM6xNtXaO6roUvNabRlclI3tLDeSHKAw,10453
|
4
|
+
universal_mcp/config.py,sha256=9yofM9MCyBiriojFVcMqRwc9njce6lgwX5r_EfvG0NE,5185
|
5
5
|
universal_mcp/exceptions.py,sha256=-pbeZhpNieJfnSd2-WM80pU8W8mK8VHXcSjky0BHwdk,665
|
6
6
|
universal_mcp/logger.py,sha256=VmH_83efpErLEDTJqz55Dp0dioTXfGvMBLZUx5smOLc,2116
|
7
7
|
universal_mcp/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
8
8
|
universal_mcp/applications/README.md,sha256=eqbizxaTxKH2O1tyIJR2yI0Db5TQxtgPd_vbpWyCa2Y,3527
|
9
9
|
universal_mcp/applications/__init__.py,sha256=l19_sMs5766VFWU_7O2niamvvvfQOteysqylbqvjjGQ,3500
|
10
|
-
universal_mcp/applications/application.py,sha256
|
10
|
+
universal_mcp/applications/application.py,sha256=_fcnfAeT5Lm2PJpHjK5ax8pCsxlDGJIKyRqnlCdEg_w,18758
|
11
|
+
universal_mcp/client/__main__.py,sha256=72e4WyJRiLoCtHlpIuvMZcDV98LvZclVaUGJmuCvpIE,886
|
12
|
+
universal_mcp/client/agent.py,sha256=20N0aZ55ge9PBg-R9gHxit2NSY1ppSz2ez8ZXTiO5kc,3803
|
13
|
+
universal_mcp/client/client.py,sha256=rx-meuWZuurjaTbjumpf5NO9zIh4wH9nMYUAp2uAejU,8393
|
14
|
+
universal_mcp/client/oauth.py,sha256=H7goK8FcVuITiwdXR30Am-qAWcgNylsrvt9EJHZMayA,4144
|
15
|
+
universal_mcp/client/token_store.py,sha256=eoZJbVcmkR-mAGhwk1tmDXXZnDKvjoqOVZG3nXfI8f8,1234
|
11
16
|
universal_mcp/integrations/README.md,sha256=lTAPXO2nivcBe1q7JT6PRa6v9Ns_ZersQMIdw-nmwEA,996
|
12
17
|
universal_mcp/integrations/__init__.py,sha256=X8iEzs02IlXfeafp6GMm-cOkg70QdjnlTRuFo24KEfo,916
|
13
|
-
universal_mcp/integrations/integration.py,sha256=
|
18
|
+
universal_mcp/integrations/integration.py,sha256=WtW92Awr111Vb_i_vKBvyyYvsNton3N2cX7wQ3ZXy2Y,13105
|
14
19
|
universal_mcp/servers/README.md,sha256=ytFlgp8-LO0oogMrHkMOp8SvFTwgsKgv7XhBVZGNTbM,2284
|
15
20
|
universal_mcp/servers/__init__.py,sha256=eBZCsaZjiEv6ZlRRslPKgurQxmpHLQyiXv2fTBygHnM,532
|
16
|
-
universal_mcp/servers/server.py,sha256=
|
21
|
+
universal_mcp/servers/server.py,sha256=FqAdAAXh23_ZL7ghZvXR5quAidXxQ6m-vDQrim40tag,12429
|
17
22
|
universal_mcp/stores/README.md,sha256=jrPh_ow4ESH4BDGaSafilhOVaN8oQ9IFlFW-j5Z5hLA,2465
|
18
23
|
universal_mcp/stores/__init__.py,sha256=quvuwhZnpiSLuojf0NfmBx2xpaCulv3fbKtKaSCEmuM,603
|
19
24
|
universal_mcp/stores/store.py,sha256=mxnmOVlDNrr8OKhENWDtCIfK7YeCBQcGdS6I2ogRCsU,6756
|
20
25
|
universal_mcp/tools/README.md,sha256=RuxliOFqV1ZEyeBdj3m8UKfkxAsfrxXh-b6V4ZGAk8I,2468
|
21
26
|
universal_mcp/tools/__init__.py,sha256=Fatza_R0qYWmNF1WQSfUZZKQFu5qf-16JhZzdmyx3KY,333
|
22
|
-
universal_mcp/tools/adapters.py,sha256=
|
27
|
+
universal_mcp/tools/adapters.py,sha256=rnicV_WBgNe0WqVG60ms0o92BoRTRqd_C9jMMndx47c,3461
|
23
28
|
universal_mcp/tools/func_metadata.py,sha256=7kUWArtUDa2Orr7VGzpwPVfyf2LM3UFA_9arMpl7Zn8,10838
|
24
29
|
universal_mcp/tools/manager.py,sha256=ao_ovTyca8HR4uwHdL_lTWNdquxcqRx6FaLA4U1lZvQ,11242
|
25
30
|
universal_mcp/tools/tools.py,sha256=8S_KzARYbG9xbyqhZcI4Wk46tXiZcWlcAMgjChXNEI4,3698
|
@@ -32,15 +37,15 @@ universal_mcp/utils/singleton.py,sha256=kolHnbS9yd5C7z-tzaUAD16GgI-thqJXysNi3sZM
|
|
32
37
|
universal_mcp/utils/testing.py,sha256=0znYkuFi8-WjOdbwrTbNC-UpMqG3EXcGOE0wxlERh_A,1464
|
33
38
|
universal_mcp/utils/openapi/__inti__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
34
39
|
universal_mcp/utils/openapi/api_generator.py,sha256=FjtvbnWuI1P8W8wXuKLCirUtsqQ4HI_TuQrhpA4SqTs,4749
|
35
|
-
universal_mcp/utils/openapi/api_splitter.py,sha256=
|
40
|
+
universal_mcp/utils/openapi/api_splitter.py,sha256=0hoq6KMcDVA5NynDBrFjjqjDugZvaWGBdfLzvyLkFAM,20968
|
36
41
|
universal_mcp/utils/openapi/docgen.py,sha256=DNmwlhg_-TRrHa74epyErMTRjV2nutfCQ7seb_Rq5hE,21366
|
37
|
-
universal_mcp/utils/openapi/openapi.py,sha256=
|
42
|
+
universal_mcp/utils/openapi/openapi.py,sha256=8TV7L1Wm9_mi_1iy0ccf_4H39XLuZjIinBnJ56H-ggw,51197
|
38
43
|
universal_mcp/utils/openapi/preprocessor.py,sha256=PPIM3Uu8DYi3dRKdqi9thr9ufeUgkr2K08ri1BwKpoQ,60835
|
39
44
|
universal_mcp/utils/openapi/readme.py,sha256=R2Jp7DUXYNsXPDV6eFTkLiy7MXbSULUj1vHh4O_nB4c,2974
|
40
45
|
universal_mcp/utils/templates/README.md.j2,sha256=Mrm181YX-o_-WEfKs01Bi2RJy43rBiq2j6fTtbWgbTA,401
|
41
46
|
universal_mcp/utils/templates/api_client.py.j2,sha256=972Im7LNUAq3yZTfwDcgivnb-b8u6_JLKWXwoIwXXXQ,908
|
42
|
-
universal_mcp-0.1.
|
43
|
-
universal_mcp-0.1.
|
44
|
-
universal_mcp-0.1.
|
45
|
-
universal_mcp-0.1.
|
46
|
-
universal_mcp-0.1.
|
47
|
+
universal_mcp-0.1.23rc2.dist-info/METADATA,sha256=oRusZjRll0c923kS-RTc64lZw_XTVk8NSuv1gKis1Lk,12154
|
48
|
+
universal_mcp-0.1.23rc2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
49
|
+
universal_mcp-0.1.23rc2.dist-info/entry_points.txt,sha256=QlBrVKmA2jIM0q-C-3TQMNJTTWOsOFQvgedBq2rZTS8,56
|
50
|
+
universal_mcp-0.1.23rc2.dist-info/licenses/LICENSE,sha256=NweDZVPslBAZFzlgByF158b85GR0f5_tLQgq1NS48To,1063
|
51
|
+
universal_mcp-0.1.23rc2.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|