universal-mcp 0.1.23rc1__py3-none-any.whl → 0.1.24rc2__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.
Files changed (47) hide show
  1. universal_mcp/analytics.py +43 -11
  2. universal_mcp/applications/application.py +186 -239
  3. universal_mcp/applications/sample_tool_app.py +80 -0
  4. universal_mcp/cli.py +5 -228
  5. universal_mcp/client/agents/__init__.py +4 -0
  6. universal_mcp/client/agents/base.py +38 -0
  7. universal_mcp/client/agents/llm.py +115 -0
  8. universal_mcp/client/agents/react.py +67 -0
  9. universal_mcp/client/cli.py +181 -0
  10. universal_mcp/client/oauth.py +218 -0
  11. universal_mcp/client/token_store.py +91 -0
  12. universal_mcp/client/transport.py +277 -0
  13. universal_mcp/config.py +201 -28
  14. universal_mcp/exceptions.py +50 -6
  15. universal_mcp/integrations/__init__.py +1 -4
  16. universal_mcp/integrations/integration.py +220 -121
  17. universal_mcp/servers/__init__.py +1 -1
  18. universal_mcp/servers/server.py +114 -247
  19. universal_mcp/stores/store.py +126 -93
  20. universal_mcp/tools/adapters.py +16 -0
  21. universal_mcp/tools/func_metadata.py +1 -1
  22. universal_mcp/tools/manager.py +15 -3
  23. universal_mcp/tools/tools.py +2 -2
  24. universal_mcp/utils/agentr.py +3 -4
  25. universal_mcp/utils/installation.py +3 -4
  26. universal_mcp/utils/openapi/api_generator.py +28 -2
  27. universal_mcp/utils/openapi/api_splitter.py +8 -19
  28. universal_mcp/utils/openapi/cli.py +243 -0
  29. universal_mcp/utils/openapi/filters.py +114 -0
  30. universal_mcp/utils/openapi/openapi.py +45 -12
  31. universal_mcp/utils/openapi/preprocessor.py +62 -7
  32. universal_mcp/utils/prompts.py +787 -0
  33. universal_mcp/utils/singleton.py +4 -1
  34. universal_mcp/utils/testing.py +6 -6
  35. universal_mcp-0.1.24rc2.dist-info/METADATA +54 -0
  36. universal_mcp-0.1.24rc2.dist-info/RECORD +53 -0
  37. universal_mcp/applications/README.md +0 -122
  38. universal_mcp/integrations/README.md +0 -25
  39. universal_mcp/servers/README.md +0 -79
  40. universal_mcp/stores/README.md +0 -74
  41. universal_mcp/tools/README.md +0 -86
  42. universal_mcp-0.1.23rc1.dist-info/METADATA +0 -283
  43. universal_mcp-0.1.23rc1.dist-info/RECORD +0 -46
  44. /universal_mcp/{utils → tools}/docstring_parser.py +0 -0
  45. {universal_mcp-0.1.23rc1.dist-info → universal_mcp-0.1.24rc2.dist-info}/WHEEL +0 -0
  46. {universal_mcp-0.1.23rc1.dist-info → universal_mcp-0.1.24rc2.dist-info}/entry_points.txt +0 -0
  47. {universal_mcp-0.1.23rc1.dist-info → universal_mcp-0.1.24rc2.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,181 @@
1
+ from typing import Any
2
+
3
+ from rich.console import Console
4
+ from rich.markdown import Markdown
5
+ from rich.panel import Panel
6
+ from rich.prompt import Prompt
7
+ from rich.table import Table
8
+ from typer import Typer
9
+
10
+ from universal_mcp.client.agents import AgentType, ReActAgent
11
+ from universal_mcp.logger import setup_logger
12
+ from universal_mcp.tools.adapters import ToolFormat
13
+ from universal_mcp.tools.manager import ToolManager
14
+
15
+ app = Typer(name="client")
16
+
17
+
18
+ class RichCLI:
19
+ def __init__(self):
20
+ self.console = Console()
21
+
22
+ def display_welcome(self, agent_name: str):
23
+ """Display welcome message"""
24
+ welcome_text = f"""
25
+ # Welcome to {agent_name}!
26
+
27
+ Available commands:
28
+ - Type your questions naturally
29
+ - `/help` - Show help
30
+ - `/tools` - List available tools
31
+ - `/switch <agent_type>` - Switch agent type (react/codeact)
32
+ - `/exit` - Exit the application
33
+ """
34
+
35
+ self.console.print(Panel(Markdown(welcome_text), title="🤖 AI Agent CLI", border_style="blue"))
36
+
37
+ def display_agent_response(self, response: str, agent_name: str):
38
+ """Display agent response with formatting"""
39
+ self.console.print(Panel(Markdown(response), title=f"🤖 {agent_name}", border_style="green", padding=(1, 2)))
40
+
41
+ def display_thinking(self, thought: str):
42
+ """Display agent's thinking process"""
43
+ if thought:
44
+ self.console.print(Panel(thought, title="💭 Thinking", border_style="yellow", padding=(1, 2)))
45
+
46
+ def display_tools(self, tools: list):
47
+ """Display available tools in a table"""
48
+ table = Table(title="🛠️ Available Tools")
49
+ table.add_column("Tool Name", style="cyan")
50
+ table.add_column("Description", style="white")
51
+
52
+ for tool in tools:
53
+ func_info = tool["function"]
54
+ table.add_row(func_info["name"], func_info["description"])
55
+
56
+ self.console.print(table)
57
+
58
+ def display_error(self, error: str):
59
+ """Display error message"""
60
+ self.console.print(Panel(error, title="❌ Error", border_style="red"))
61
+
62
+ def get_user_input(self) -> str:
63
+ """Get user input with rich prompt"""
64
+ return Prompt.ask("[bold blue]You[/bold blue]", console=self.console)
65
+
66
+ def display_info(self, message: str):
67
+ """Display info message"""
68
+ self.console.print(f"[bold cyan]ℹ️ {message}[/bold cyan]")
69
+
70
+
71
+ class AgentCLI:
72
+ def __init__(self):
73
+ self.cli = RichCLI()
74
+ self.tool_manager = ToolManager(default_format=ToolFormat.OPENAI)
75
+ self.current_agent = None
76
+ self.agent_type = AgentType.REACT
77
+ # self.tool_manager.register_tools_from_app(SampleToolApp(), tags=["all"])
78
+
79
+ def create_agent(self, agent_type: AgentType, model: str = "gpt-4o") -> Any:
80
+ """Create an agent based on type"""
81
+ instructions = """You are a helpful AI assistant. Use the available tools to help users with their requests.
82
+ Think step by step and provide clear explanations of your reasoning."""
83
+
84
+ if agent_type == AgentType.REACT:
85
+ return ReActAgent("ReAct Agent", instructions, model)
86
+ else:
87
+ raise ValueError("Unknown agent type")
88
+
89
+ def switch_agent(self, agent_type: str):
90
+ """Switch to a different agent type"""
91
+ try:
92
+ self.agent_type = AgentType(agent_type.lower())
93
+ self.current_agent = self.create_agent(self.agent_type)
94
+ self.cli.display_info(f"Switched to {self.agent_type.value} agent")
95
+ except ValueError:
96
+ self.cli.display_error(f"Unknown agent type: {agent_type}")
97
+
98
+ async def process_command(self, user_input: str) -> bool | None:
99
+ """Process special commands, return True if command was processed"""
100
+ if user_input.startswith("/"):
101
+ command_parts = user_input[1:].split()
102
+ command = command_parts[0].lower()
103
+
104
+ if command == "help":
105
+ self.show_help()
106
+ return True
107
+ elif command == "tools":
108
+ self.cli.display_tools(self.tool_manager.list_tools())
109
+ return True
110
+ elif command == "switch" and len(command_parts) > 1:
111
+ self.switch_agent(command_parts[1])
112
+ return True
113
+ elif command == "exit" or command == "quit" or command == "q":
114
+ self.cli.display_info("Goodbye! 👋")
115
+ return False
116
+ else:
117
+ self.cli.display_error(f"Unknown command: {command}")
118
+ return True
119
+
120
+ return None # Not a command
121
+
122
+ def show_help(self):
123
+ """Show help information"""
124
+ help_text = """
125
+ # Available Commands
126
+
127
+ - `/help` - Show this help message
128
+ - `/tools` - List all available tools
129
+ - `/exit` - Exit the application
130
+ "
131
+ """
132
+ self.cli.console.print(help_text)
133
+
134
+ async def run(self, model):
135
+ """Main application loop"""
136
+
137
+ # Initialize agent
138
+ self.current_agent = self.create_agent(self.agent_type, model)
139
+
140
+ # Display welcome
141
+ self.cli.display_welcome(self.current_agent.name)
142
+
143
+ # Main loop
144
+ while True:
145
+ try:
146
+ user_input = self.cli.get_user_input()
147
+
148
+ if not user_input.strip():
149
+ continue
150
+
151
+ # Process commands
152
+ command_result = await self.process_command(user_input)
153
+ if command_result is False: # Exit command
154
+ break
155
+ elif command_result is True: # Command processed
156
+ continue
157
+
158
+ # Process with agent
159
+ response = await self.current_agent.process_step(user_input, self.tool_manager)
160
+
161
+ self.cli.display_agent_response(response, self.current_agent.name)
162
+
163
+ except KeyboardInterrupt:
164
+ self.cli.display_info("\nGoodbye! 👋")
165
+ break
166
+ except Exception as e:
167
+ self.cli.display_error(f"An error occurred: {str(e)}")
168
+
169
+
170
+ @app.command()
171
+ def run(model: str = "openrouter/auto"):
172
+ """Run the agent CLI"""
173
+ import asyncio
174
+
175
+ setup_logger(log_file=None, level="WARNING")
176
+ agent_cli = AgentCLI()
177
+ asyncio.run(agent_cli.run(model=model))
178
+
179
+
180
+ if __name__ == "__main__":
181
+ app()
@@ -0,0 +1,218 @@
1
+ import threading
2
+ import time
3
+ from http.server import BaseHTTPRequestHandler, HTTPServer
4
+ from typing import Any
5
+ from urllib.parse import parse_qs, urlparse
6
+
7
+ from universal_mcp.utils.singleton import Singleton
8
+
9
+
10
+ class CallbackHandler(BaseHTTPRequestHandler):
11
+ """Handles the HTTP GET request for an OAuth 2.0 callback.
12
+
13
+ This handler is designed to capture the authorization code and state
14
+ (or an error) returned by an OAuth 2.0 authorization server as query
15
+ parameters in the redirect URI. It stores these values in a shared
16
+ `callback_data` dictionary.
17
+
18
+ It sends a simple HTML response to the user's browser indicating
19
+ success or failure of the authorization attempt.
20
+ """
21
+
22
+ def __init__(self, request, client_address, server, callback_data: dict):
23
+ """Initializes the CallbackHandler.
24
+
25
+ Args:
26
+ request: The HTTP request.
27
+ client_address: The client's address.
28
+ server: The server instance.
29
+ callback_data (dict): A dictionary shared with the `CallbackServer`
30
+ to store the captured OAuth parameters (e.g.,
31
+ `authorization_code`, `state`, `error`).
32
+ """
33
+ self.callback_data = callback_data
34
+ super().__init__(request, client_address, server)
35
+
36
+ def do_GET(self):
37
+ """Handles the GET request from the OAuth authorization server's redirect.
38
+
39
+ Parses the URL query parameters to find 'code' and 'state', or 'error'.
40
+ Stores these values into the `self.callback_data` dictionary.
41
+ Responds to the browser with a success or failure HTML page.
42
+ """
43
+ parsed = urlparse(self.path)
44
+ query_params = parse_qs(parsed.query)
45
+
46
+ if "code" in query_params:
47
+ self.callback_data["authorization_code"] = query_params["code"][0]
48
+ self.callback_data["state"] = query_params.get("state", [None])[0]
49
+ self.send_response(200)
50
+ self.send_header("Content-type", "text/html")
51
+ self.end_headers()
52
+ self.wfile.write(b"""
53
+ <html>
54
+ <body>
55
+ <h1>Authorization Successful!</h1>
56
+ <p>You can close this window and return to the terminal.</p>
57
+ <script>setTimeout(() => window.close(), 2000);</script>
58
+ </body>
59
+ </html>
60
+ """)
61
+ elif "error" in query_params:
62
+ self.callback_data["error"] = query_params["error"][0]
63
+ self.send_response(400)
64
+ self.send_header("Content-type", "text/html")
65
+ self.end_headers()
66
+ self.wfile.write(
67
+ f"""
68
+ <html>
69
+ <body>
70
+ <h1>Authorization Failed</h1>
71
+ <p>Error: {query_params["error"][0]}</p>
72
+ <p>You can close this window and return to the terminal.</p>
73
+ </body>
74
+ </html>
75
+ """.encode()
76
+ )
77
+ else:
78
+ self.send_response(404)
79
+ self.end_headers()
80
+
81
+ def log_message(self, format: str, *args: Any):
82
+ """Suppresses the default logging of HTTP requests.
83
+
84
+ Overrides the base class method to prevent request logs from being
85
+ printed to stderr, keeping the console cleaner during the OAuth flow.
86
+ """
87
+ pass
88
+
89
+
90
+ class CallbackServer(metaclass=Singleton):
91
+ """A singleton HTTP server to manage OAuth 2.0 redirect callbacks.
92
+
93
+ This server runs in a background thread, listening on a specified
94
+ localhost port. It uses the `CallbackHandler` to capture the
95
+ authorization code or error returned by an OAuth 2.0 provider
96
+ after user authentication.
97
+
98
+ Being a Singleton, only one instance of this server will run per
99
+ application, even if instantiated multiple times.
100
+
101
+ Attributes:
102
+ port (int): The port number on localhost where the server listens.
103
+ server (HTTPServer | None): The underlying `HTTPServer` instance.
104
+ None if the server is not running.
105
+ thread (threading.Thread | None): The background thread in which
106
+ the server runs. None if the server is not running.
107
+ callback_data (dict): A dictionary to store data received from the
108
+ OAuth callback (e.g., `authorization_code`, `state`, `error`).
109
+ This is shared with the `CallbackHandler`.
110
+ _running (bool): A flag indicating whether the server is currently
111
+ started and listening.
112
+ """
113
+
114
+ def __init__(self, port: int = 3000):
115
+ """Initializes the CallbackServer.
116
+
117
+ Args:
118
+ port (int, optional): The port number on localhost for the server
119
+ to listen on. Defaults to 3000.
120
+ """
121
+ self.port = port
122
+ self.server = None
123
+ self.thread = None
124
+ self.callback_data = {"authorization_code": None, "state": None, "error": None}
125
+ self._running = False
126
+
127
+ @property
128
+ def is_running(self) -> bool:
129
+ return self._running
130
+
131
+ def _create_handler_with_data(self):
132
+ """Creates a `CallbackHandler` subclass with shared `callback_data`.
133
+
134
+ This method dynamically defines a new handler class that inherits from
135
+ `CallbackHandler`. The purpose is to allow the handler instances
136
+ to access and modify the `self.callback_data` dictionary of this
137
+ `CallbackServer` instance, enabling communication of OAuth parameters
138
+ from the handler back to the server logic.
139
+
140
+ Returns:
141
+ type: A new class, subclass of `CallbackHandler`.
142
+ """
143
+ callback_data = self.callback_data
144
+
145
+ class DataCallbackHandler(CallbackHandler):
146
+ def __init__(self, request, client_address, server):
147
+ super().__init__(request, client_address, server, callback_data)
148
+
149
+ return DataCallbackHandler
150
+
151
+ def start(self):
152
+ """Starts the HTTP callback server in a background daemon thread.
153
+
154
+ If the server is not already running, it initializes an `HTTPServer`
155
+ with a specialized `CallbackHandler` and starts it in a new
156
+ daemon thread. This allows the main application flow to continue
157
+ while waiting for the OAuth callback.
158
+ """
159
+ if self._running:
160
+ return
161
+ handler_class = self._create_handler_with_data()
162
+ self.server = HTTPServer(("localhost", self.port), handler_class)
163
+ self.thread = threading.Thread(target=self.server.serve_forever, daemon=True)
164
+ self.thread.start()
165
+ print(f"🖥️ Started callback server on http://localhost:{self.port}")
166
+ self._running = True
167
+
168
+ def stop(self):
169
+ """Stops the HTTP callback server and cleans up resources.
170
+
171
+ Shuts down the `HTTPServer` and waits for its background thread
172
+ to complete.
173
+ """
174
+ if self.server:
175
+ self.server.shutdown()
176
+ self.server.server_close()
177
+ if self.thread:
178
+ self.thread.join(timeout=1)
179
+
180
+ def wait_for_callback(self, timeout: int = 300) -> str:
181
+ """Waits for the OAuth callback to provide an authorization code.
182
+
183
+ This method polls the `self.callback_data` dictionary until an
184
+ authorization code is received or an error is reported by the
185
+ `CallbackHandler`, or until the timeout is reached.
186
+
187
+ Args:
188
+ timeout (int, optional): The maximum time in seconds to wait
189
+ for the callback. Defaults to 300 seconds (5 minutes).
190
+
191
+ Returns:
192
+ str: The received authorization code.
193
+
194
+ Raises:
195
+ Exception: If an error is reported in the callback
196
+ (e.g., "OAuth error: <error_message>").
197
+ Exception: If the timeout is reached before a code or error
198
+ is received (e.g., "Timeout waiting for OAuth callback").
199
+ """
200
+ start_time = time.time()
201
+ while time.time() - start_time < timeout:
202
+ if self.callback_data["authorization_code"]:
203
+ return self.callback_data["authorization_code"]
204
+ elif self.callback_data["error"]:
205
+ raise Exception(f"OAuth error: {self.callback_data['error']}")
206
+ time.sleep(0.1)
207
+ raise Exception("Timeout waiting for OAuth callback")
208
+
209
+ def get_state(self) -> str | None:
210
+ """Retrieves the 'state' parameter received during the OAuth callback.
211
+
212
+ The state parameter is often used to prevent cross-site request forgery (CSRF)
213
+ attacks by matching its value with one sent in the initial authorization request.
214
+
215
+ Returns:
216
+ str | None: The 'state' parameter value if received, otherwise None.
217
+ """
218
+ return self.callback_data["state"]
@@ -0,0 +1,91 @@
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
+ """Persistent storage for OAuth tokens and client information using KeyringStore.
10
+
11
+ This class implements the `mcp.client.auth.TokenStorage` interface,
12
+ providing a mechanism to securely store and retrieve OAuth 2.0 tokens
13
+ (as `OAuthToken` objects) and OAuth client registration details
14
+ (as `OAuthClientInformationFull` objects).
15
+
16
+ It utilizes an underlying `KeyringStore` instance, which typically
17
+ delegates to the operating system's secure credential management
18
+ system (e.g., macOS Keychain, Windows Credential Manager, Linux KWallet).
19
+ This ensures that sensitive token data is stored securely and persistently.
20
+
21
+ Attributes:
22
+ store (KeyringStore): The `KeyringStore` instance used for actually
23
+ storing and retrieving the serialized token and client info data.
24
+ """
25
+
26
+ def __init__(self, store: KeyringStore):
27
+ """Initializes the TokenStore.
28
+
29
+ Args:
30
+ store (KeyringStore): An instance of `KeyringStore` that will be
31
+ used for the actual persistence of tokens and client information.
32
+ """
33
+ self.store = store
34
+ # These are not meant to be persistent caches in this implementation
35
+ # self._tokens: OAuthToken | None = None
36
+ # self._client_info: OAuthClientInformationFull | None = None
37
+
38
+ async def get_tokens(self) -> OAuthToken | None:
39
+ """Retrieves OAuth tokens from the persistent KeyringStore.
40
+
41
+ Fetches the JSON string representation of tokens from the store using
42
+ the key "tokens" and deserializes it into an `OAuthToken` object.
43
+
44
+ Returns:
45
+ OAuthToken | None: The deserialized `OAuthToken` object if found
46
+ and successfully parsed, otherwise None.
47
+ """
48
+ try:
49
+ return OAuthToken.model_validate_json(self.store.get("tokens"))
50
+ except KeyNotFoundError:
51
+ return None
52
+
53
+ async def set_tokens(self, tokens: OAuthToken) -> None:
54
+ """Serializes OAuth tokens to JSON and saves them to the KeyringStore.
55
+
56
+ The provided `OAuthToken` object is converted to its JSON string
57
+ representation and stored in the `KeyringStore` under the key "tokens".
58
+
59
+ Args:
60
+ tokens (OAuthToken): The `OAuthToken` object to store.
61
+ """
62
+ self.store.set("tokens", tokens.model_dump_json())
63
+
64
+ async def get_client_info(self) -> OAuthClientInformationFull | None:
65
+ """Retrieves OAuth client information from the persistent KeyringStore.
66
+
67
+ Fetches the JSON string representation of client information from the
68
+ store using the key "client_info" and deserializes it into an
69
+ `OAuthClientInformationFull` object.
70
+
71
+ Returns:
72
+ OAuthClientInformationFull | None: The deserialized object if found
73
+ and successfully parsed, otherwise None.
74
+ """
75
+ try:
76
+ return OAuthClientInformationFull.model_validate_json(self.store.get("client_info"))
77
+ except KeyNotFoundError:
78
+ return None
79
+
80
+ async def set_client_info(self, client_info: OAuthClientInformationFull) -> None:
81
+ """Serializes OAuth client information to JSON and saves it to KeyringStore.
82
+
83
+ The provided `OAuthClientInformationFull` object is converted to its
84
+ JSON string representation and stored in the `KeyringStore` under the
85
+ key "client_info".
86
+
87
+ Args:
88
+ client_info (OAuthClientInformationFull): The client information object
89
+ to store.
90
+ """
91
+ self.store.set("client_info", client_info.model_dump_json())