universal-mcp 0.1.23rc2__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 (48) hide show
  1. universal_mcp/analytics.py +43 -11
  2. universal_mcp/applications/application.py +186 -132
  3. universal_mcp/applications/sample_tool_app.py +80 -0
  4. universal_mcp/cli.py +5 -229
  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 +122 -18
  11. universal_mcp/client/token_store.py +62 -3
  12. universal_mcp/client/{client.py → transport.py} +127 -48
  13. universal_mcp/config.py +160 -46
  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/func_metadata.py +1 -1
  21. universal_mcp/tools/manager.py +15 -3
  22. universal_mcp/tools/tools.py +2 -2
  23. universal_mcp/utils/agentr.py +3 -4
  24. universal_mcp/utils/installation.py +3 -4
  25. universal_mcp/utils/openapi/api_generator.py +28 -2
  26. universal_mcp/utils/openapi/api_splitter.py +0 -1
  27. universal_mcp/utils/openapi/cli.py +243 -0
  28. universal_mcp/utils/openapi/filters.py +114 -0
  29. universal_mcp/utils/openapi/openapi.py +31 -2
  30. universal_mcp/utils/openapi/preprocessor.py +62 -7
  31. universal_mcp/utils/prompts.py +787 -0
  32. universal_mcp/utils/singleton.py +4 -1
  33. universal_mcp/utils/testing.py +6 -6
  34. universal_mcp-0.1.24rc2.dist-info/METADATA +54 -0
  35. universal_mcp-0.1.24rc2.dist-info/RECORD +53 -0
  36. universal_mcp/applications/README.md +0 -122
  37. universal_mcp/client/__main__.py +0 -30
  38. universal_mcp/client/agent.py +0 -96
  39. universal_mcp/integrations/README.md +0 -25
  40. universal_mcp/servers/README.md +0 -79
  41. universal_mcp/stores/README.md +0 -74
  42. universal_mcp/tools/README.md +0 -86
  43. universal_mcp-0.1.23rc2.dist-info/METADATA +0 -283
  44. universal_mcp-0.1.23rc2.dist-info/RECORD +0 -51
  45. /universal_mcp/{utils → tools}/docstring_parser.py +0 -0
  46. {universal_mcp-0.1.23rc2.dist-info → universal_mcp-0.1.24rc2.dist-info}/WHEEL +0 -0
  47. {universal_mcp-0.1.23rc2.dist-info → universal_mcp-0.1.24rc2.dist-info}/entry_points.txt +0 -0
  48. {universal_mcp-0.1.23rc2.dist-info → universal_mcp-0.1.24rc2.dist-info}/licenses/LICENSE +0 -0
@@ -1,7 +1,7 @@
1
1
  import os
2
2
  import webbrowser
3
3
  from contextlib import AsyncExitStack
4
- from typing import Any, Literal
4
+ from typing import Any, Literal, Self
5
5
 
6
6
  from loguru import logger
7
7
  from mcp import ClientSession, StdioServerParameters
@@ -9,7 +9,6 @@ from mcp.client.auth import OAuthClientProvider
9
9
  from mcp.client.sse import sse_client
10
10
  from mcp.client.stdio import stdio_client
11
11
  from mcp.client.streamable_http import streamablehttp_client
12
- from mcp.server import Server
13
12
  from mcp.shared.auth import OAuthClientMetadata
14
13
  from mcp.types import (
15
14
  CallToolResult as MCPCallToolResult,
@@ -21,13 +20,20 @@ from openai.types.chat import ChatCompletionToolParam
21
20
 
22
21
  from universal_mcp.client.oauth import CallbackServer
23
22
  from universal_mcp.client.token_store import TokenStore
24
- from universal_mcp.config import ClientTransportConfig
23
+ from universal_mcp.config import ClientConfig, ClientTransportConfig
25
24
  from universal_mcp.stores.store import KeyringStore
26
25
  from universal_mcp.tools.adapters import transform_mcp_tool_to_openai_tool
27
26
 
28
27
 
29
- class MCPClient:
30
- """Manages MCP server connections and tool execution."""
28
+ class ClientTransport:
29
+ """
30
+ Client for connecting to and interacting with a single MCP server.
31
+
32
+ Manages the lifecycle of a connection to an MCP server, handles various
33
+ transport mechanisms (stdio, sse, streamable_http), and facilitates
34
+ authentication, including OAuth 2.0 client flows. Allows listing tools
35
+ available on the server and calling them.
36
+ """
31
37
 
32
38
  def __init__(self, name: str, config: ClientTransportConfig) -> None:
33
39
  self.name: str = name
@@ -35,14 +41,12 @@ class MCPClient:
35
41
  self.session: ClientSession | None = None
36
42
  self.server_url: str = config.url
37
43
 
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(
44
+ # Create OAuth authentication handler if needed
45
+ if self.server_url and not getattr(self.config, "headers", None):
46
+ # Set up callback server
47
+ self._callback_server = CallbackServer(port=3000)
48
+ self.store: KeyringStore | None = KeyringStore(self.name)
49
+ self.auth: OAuthClientProvider | None = OAuthClientProvider(
46
50
  server_url="/".join(self.server_url.split("/")[:-1]),
47
51
  client_metadata=OAuthClientMetadata.model_validate(self.client_metadata_dict),
48
52
  storage=TokenStore(self.store),
@@ -50,10 +54,18 @@ class MCPClient:
50
54
  callback_handler=self._callback_handler,
51
55
  )
52
56
  else:
57
+ self._callback_server = None
58
+ self.store = None
53
59
  self.auth = None
54
60
 
61
+ @property
62
+ def callback_server(self) -> CallbackServer:
63
+ if self._callback_server and not self._callback_server.is_running:
64
+ self._callback_server.start()
65
+ return self._callback_server
66
+
55
67
  async def _callback_handler(self) -> tuple[str, str | None]:
56
- """Wait for OAuth callback and return auth code and state."""
68
+ """Handles the OAuth callback by waiting for and returning auth details."""
57
69
  print("⏳ Waiting for authorization callback...")
58
70
  try:
59
71
  auth_code = self.callback_server.wait_for_callback(timeout=300)
@@ -63,38 +75,45 @@ class MCPClient:
63
75
 
64
76
  @property
65
77
  def client_metadata_dict(self) -> dict[str, Any]:
78
+ """Provides OAuth 2.0 client metadata for registration or authentication."""
66
79
  return {
67
- "client_name": "Simple Auth Client",
68
- "redirect_uris": ["http://localhost:3000/callback"],
80
+ "client_name": self.name,
81
+ "redirect_uris": [self.callback_server.redirect_uri], # type: ignore
69
82
  "grant_types": ["authorization_code", "refresh_token"],
70
83
  "response_types": ["code"],
71
84
  "token_endpoint_auth_method": "client_secret_post",
72
85
  }
73
86
 
74
87
  async def _default_redirect_handler(self, authorization_url: str) -> None:
75
- """Default redirect handler that opens the URL in a browser."""
88
+ """Default handler for OAuth redirects; opens URL in a web browser."""
76
89
  print(f"Opening browser for authorization: {authorization_url}")
77
90
  webbrowser.open(authorization_url)
78
91
 
79
- async def initialize(self, exit_stack: AsyncExitStack):
80
- """Initialize the server connection."""
81
- transport = self.config.transport
92
+ async def initialize(self, exit_stack: AsyncExitStack) -> None:
93
+ """
94
+ Establishes and initializes the connection to the MCP server.
95
+
96
+ Raises:
97
+ ValueError: If the transport type is unknown or if required
98
+ configuration for a transport is missing.
99
+ """
100
+ transport = getattr(self.config, "transport", None)
101
+ session = None
82
102
  try:
83
103
  if transport == "stdio":
84
- command = self.config["command"]
85
- if command is None:
104
+ command = self.config.get("command")
105
+ if not command:
86
106
  raise ValueError("The command must be a valid string and cannot be None.")
87
107
 
88
108
  server_params = StdioServerParameters(
89
109
  command=command,
90
- args=self.config["args"],
91
- env={**os.environ, **self.config["env"]} if self.config.get("env") else None,
110
+ args=self.config.get("args", []),
111
+ env={**os.environ, **self.config.get("env", {})} if self.config.get("env") else None,
92
112
  )
93
113
  stdio_transport = await exit_stack.enter_async_context(stdio_client(server_params))
94
114
  read, write = stdio_transport
95
115
  session = await exit_stack.enter_async_context(ClientSession(read, write))
96
116
  await session.initialize()
97
- self.session = session
98
117
  elif transport == "streamable_http":
99
118
  url = self.config.get("url")
100
119
  headers = self.config.get("headers", {})
@@ -106,10 +125,9 @@ class MCPClient:
106
125
  read, write, _ = streamable_http_transport
107
126
  session = await exit_stack.enter_async_context(ClientSession(read, write))
108
127
  await session.initialize()
109
- self.session = session
110
128
  elif transport == "sse":
111
- url = self.config.url
112
- headers = self.config.headers
129
+ url = self.config.get("url")
130
+ headers = self.config.get("headers", {})
113
131
  if not url:
114
132
  raise ValueError("'url' must be provided for sse transport.")
115
133
  sse_transport = await exit_stack.enter_async_context(
@@ -118,73 +136,126 @@ class MCPClient:
118
136
  read, write = sse_transport
119
137
  session = await exit_stack.enter_async_context(ClientSession(read, write))
120
138
  await session.initialize()
121
- self.session = session
122
139
  else:
123
140
  raise ValueError(f"Unknown transport: {transport}")
141
+ self.session = session
124
142
  except Exception as e:
143
+ if session:
144
+ await session.aclose()
125
145
  logger.error(f"Error initializing server {self.name}: {e}")
126
146
  raise
127
147
 
128
148
  async def list_tools(self) -> list[MCPTool]:
129
- """List available tools from the server."""
149
+ """Lists all tools available on the connected MCP server."""
130
150
  if self.session:
131
- tools = await self.session.list_tools()
132
- return list(tools.tools)
151
+ try:
152
+ tools = await self.session.list_tools()
153
+ return list(tools.tools)
154
+ except Exception as e:
155
+ logger.warning(f"Failed to list tools for client {self.name}: {e}")
133
156
  return []
134
157
 
135
158
  async def call_tool(self, tool_name: str, arguments: dict[str, Any]) -> MCPCallToolResult:
136
- """Call a tool on the server."""
159
+ """Calls a specified tool on the connected MCP server with given arguments."""
137
160
  if self.session:
138
- return await self.session.call_tool(tool_name, arguments)
161
+ try:
162
+ return await self.session.call_tool(tool_name, arguments)
163
+ except Exception as e:
164
+ logger.error(f"Error calling tool '{tool_name}' on client {self.name}: {e}")
139
165
  return MCPCallToolResult(
140
166
  content=[],
141
167
  isError=True,
142
168
  )
143
169
 
144
170
 
145
- class MultiClientServer(Server):
171
+ class MultiClientTransport:
146
172
  """
147
- Manages multiple MCP servers and maintains a mapping from tool name to the server that provides it.
173
+ Aggregates multiple ClientTransport instances to act as a single MCP Server.
174
+
175
+ Provides a unified Server interface for a collection of ClientTransport
176
+ instances, each potentially connected to a different MCP server.
177
+ Maintains a mapping of tool names to the specific ClientTransport that
178
+ provides that tool.
148
179
  """
149
180
 
150
181
  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] = {}
182
+ self.clients: list[ClientTransport] = [ClientTransport(name, config) for name, config in clients.items()]
183
+ self.tool_to_client: dict[str, ClientTransport] = {}
153
184
  self._mcp_tools: list[MCPTool] = []
154
185
  self._exit_stack: AsyncExitStack = AsyncExitStack()
155
186
 
187
+ @classmethod
188
+ def from_file(cls, path: str) -> Self:
189
+ mcp_config = ClientConfig.load_json_config(path)
190
+ return cls(mcp_config.mcpServers)
191
+
192
+ def save_to_file(self, path: str) -> None:
193
+ mcp_config = ClientConfig(mcpServers={name: config.model_dump() for name, config in self.clients.items()})
194
+ mcp_config.save_json_config(path)
195
+
196
+ async def add_client(self, name: str, config: ClientTransportConfig) -> None:
197
+ if name in self.tool_to_client:
198
+ logger.warning(f"Client {name} already exists. Skipping.")
199
+ return
200
+ self.clients.append(ClientTransport(name, config))
201
+ self.tool_to_client[name] = self.clients[-1]
202
+ logger.info(f"Added client: {name}")
203
+ await self._populate_tool_mapping()
204
+
205
+ async def remove_client(self, name: str) -> None:
206
+ if name not in self.tool_to_client:
207
+ logger.warning(f"Client {name} not found. Skipping.")
208
+ return
209
+ self.clients.remove(self.tool_to_client[name])
210
+ del self.tool_to_client[name]
211
+ logger.info(f"Removed client: {name}")
212
+ await self._populate_tool_mapping()
213
+
156
214
  async def __aenter__(self):
157
- """Initialize the server connection."""
158
215
  for client in self.clients:
159
216
  await client.initialize(self._exit_stack)
160
217
  await self._populate_tool_mapping()
161
218
  return self
162
219
 
163
220
  async def __aexit__(self, exc_type, exc_val, exc_tb):
164
- """Clean up the server connection."""
165
221
  self.clients.clear()
166
222
  self.tool_to_client.clear()
167
223
  self._mcp_tools.clear()
168
224
  await self._exit_stack.aclose()
169
225
 
170
226
  async def _populate_tool_mapping(self):
171
- """Populate the mapping from tool name to server."""
172
227
  self.tool_to_client.clear()
173
228
  self._mcp_tools.clear()
174
229
  for client in self.clients:
175
230
  try:
176
231
  tools = await client.list_tools()
177
232
  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}")
233
+ tool_name = getattr(tool, "name", None)
181
234
  if tool_name:
182
- self.tool_to_client[tool_name] = client
235
+ if tool_name not in self.tool_to_client:
236
+ self._mcp_tools.append(tool)
237
+ self.tool_to_client[tool_name] = client
238
+ logger.info(f"Found tool: {tool_name} from client: {client.name}")
239
+ else:
240
+ logger.warning(
241
+ f"Duplicate tool name '{tool_name}' found in client '{client.name}'. Skipping."
242
+ )
183
243
  except Exception as e:
184
244
  logger.warning(f"Failed to list tools for client {client.name}: {e}")
185
245
 
186
246
  async def list_tools(self, format: Literal["mcp", "openai"] = "mcp") -> list[MCPTool | ChatCompletionToolParam]:
187
- """List available tools from all servers."""
247
+ """
248
+ Lists all unique tools available from all managed clients.
249
+
250
+ Args:
251
+ format: The desired format for the returned tools.
252
+
253
+ Returns:
254
+ List of tools in the specified format.
255
+
256
+ Raises:
257
+ ValueError: If an unsupported format is requested.
258
+ """
188
259
  if format == "mcp":
189
260
  return self._mcp_tools
190
261
  elif format == "openai":
@@ -193,6 +264,14 @@ class MultiClientServer(Server):
193
264
  raise ValueError(f"Invalid format: {format}")
194
265
 
195
266
  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]
267
+ """
268
+ Calls a tool by routing the request to the appropriate ClientTransport.
269
+
270
+ Raises:
271
+ KeyError: If the tool_name is not found.
272
+ """
273
+ client = self.tool_to_client.get(tool_name)
274
+ if not client:
275
+ logger.error(f"Tool '{tool_name}' not found in any client.")
276
+ return MCPCallToolResult(content=[], isError=True)
198
277
  return await client.call_tool(tool_name, arguments)
universal_mcp/config.py CHANGED
@@ -7,38 +7,83 @@ from pydantic_settings import BaseSettings, SettingsConfigDict
7
7
 
8
8
 
9
9
  class StoreConfig(BaseModel):
10
- """Configuration for credential storage."""
10
+ """Specifies the configuration for a credential or token store.
11
11
 
12
- name: str = Field(default="universal_mcp", description="Name of the store")
12
+ Defines where and how sensitive information like API keys or OAuth tokens
13
+ should be stored and retrieved.
14
+ """
15
+
16
+ name: str = Field(
17
+ default="universal_mcp",
18
+ description="Name of the store service or context (e.g., 'my_app_tokens', 'global_api_keys').",
19
+ )
13
20
  type: Literal["memory", "environment", "keyring", "agentr"] = Field(
14
- default="memory", description="Type of credential storage to use"
21
+ default="memory",
22
+ description="The type of storage backend to use. 'memory' is transient, 'environment' uses environment variables, 'keyring' uses the system's secure credential manager, 'agentr' delegates to AgentR platform storage.",
23
+ )
24
+ path: Path | None = Field(
25
+ default=None,
26
+ description="Filesystem path for store types that require it (e.g., a future 'file' store type). Currently not used by memory, environment, or keyring.",
15
27
  )
16
- path: Path | None = Field(default=None, description="Path to store credentials (if applicable)")
17
28
 
18
29
 
19
30
  class IntegrationConfig(BaseModel):
20
- """Configuration for API integrations."""
31
+ """Defines the authentication and credential management for an application.
21
32
 
22
- name: str = Field(..., description="Name of the integration")
33
+ Specifies how a particular application (`AppConfig`) should authenticate
34
+ with its target service, including the authentication type (e.g., API key,
35
+ OAuth) and where to find the necessary credentials.
36
+ """
37
+
38
+ name: str = Field(
39
+ ..., description="A unique name for this integration instance (e.g., 'my_github_oauth', 'tavily_api_key')."
40
+ )
23
41
  type: Literal["api_key", "oauth", "agentr", "oauth2", "basic_auth"] = Field(
24
- default="api_key", description="Type of authentication to use"
42
+ default="api_key",
43
+ description="The authentication mechanism to be used. 'oauth2' is often synonymous with 'oauth'. 'agentr' implies AgentR platform managed authentication.",
44
+ )
45
+ credentials: dict[str, Any] | None = Field(
46
+ default=None,
47
+ description="Directly provided credentials, if not using a store. Structure depends on the integration type (e.g., {'api_key': 'value'} or {'client_id': 'id', 'client_secret': 'secret'}). Use with caution for sensitive data; prefer using a 'store'.",
48
+ )
49
+ store: StoreConfig | None = Field(
50
+ default=None,
51
+ description="Configuration for the credential store to be used for this integration, overriding any default server-level store.",
25
52
  )
26
- credentials: dict[str, Any] | None = Field(default=None, description="Integration-specific credentials")
27
- store: StoreConfig | None = Field(default=None, description="Store configuration for credentials")
28
53
 
29
54
 
30
55
  class AppConfig(BaseModel):
31
- """Configuration for individual applications."""
56
+ """Configuration for a single application to be loaded by the MCP server.
32
57
 
33
- name: str = Field(..., description="Name of the application")
34
- integration: IntegrationConfig | None = Field(default=None, description="Integration configuration")
35
- actions: list[str] | None = Field(default=None, description="List of available actions")
58
+ Defines an application's name (slug), its integration settings for
59
+ authentication, and optionally, a list of specific actions (tools)
60
+ it provides.
61
+ """
62
+
63
+ name: str = Field(
64
+ ...,
65
+ description="The unique name or slug of the application (e.g., 'github', 'google-calendar'). This is often used to dynamically load the application module.",
66
+ )
67
+ integration: IntegrationConfig | None = Field(
68
+ default=None,
69
+ description="Authentication and credential configuration for this application. If None, the application is assumed not to require authentication or uses a global/default mechanism.",
70
+ )
71
+ actions: list[str] | None = Field(
72
+ default=None,
73
+ description="A list of specific actions or tools provided by this application that should be exposed. If None or empty, all tools from the application might be exposed by default, depending on the application's implementation.",
74
+ )
36
75
 
37
76
 
38
77
  class ServerConfig(BaseSettings):
39
- """Main server configuration."""
78
+ """Core configuration settings for the Universal MCP server.
79
+
80
+ Manages server behavior, including its name, description, connection
81
+ to AgentR (if applicable), transport protocol, network settings (port/host),
82
+ applications to load, default credential store, and logging verbosity.
83
+ Settings can be loaded from environment variables or a .env file.
84
+ """
40
85
 
41
- model_config = SettingsConfigDict(
86
+ model_config: SettingsConfigDict = SettingsConfigDict(
42
87
  env_file=".env",
43
88
  env_file_encoding="utf-8",
44
89
  case_sensitive=True,
@@ -46,24 +91,50 @@ class ServerConfig(BaseSettings):
46
91
  )
47
92
 
48
93
  name: str = Field(default="Universal MCP", description="Name of the MCP server")
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"
94
+ description: str = Field(
95
+ default="Universal MCP", description="A brief description of this MCP server's purpose or deployment."
96
+ )
97
+ type: Literal["local", "agentr", "other"] = Field(
98
+ default="agentr",
99
+ description="Deployment type of the server. 'local' runs apps defined in 'apps' list; 'agentr' dynamically loads apps from the AgentR platform.",
52
100
  )
53
- api_key: SecretStr | None = Field(default=None, description="API key for authentication", alias="AGENTR_API_KEY")
54
- type: Literal["local", "agentr"] = Field(default="agentr", description="Type of server deployment")
55
101
  transport: Literal["stdio", "sse", "streamable-http"] = Field(
56
- default="stdio", description="Transport protocol to use"
102
+ default="stdio",
103
+ description="The communication protocol the server will use to interact with clients (e.g., an AI agent).",
104
+ )
105
+ port: int = Field(
106
+ default=8005,
107
+ description="Network port for 'sse' or 'streamable-http' transports. Must be between 1 and 65535.",
108
+ ge=1,
109
+ le=65535,
110
+ )
111
+ log_level: str = Field(
112
+ default="INFO", description="Logging level for the server (e.g., DEBUG, INFO, WARNING, ERROR, CRITICAL)."
113
+ )
114
+ # AgentR specific settings
115
+ base_url: str = Field(
116
+ default="https://api.agentr.dev",
117
+ description="The base URL for the AgentR API, used when type is 'agentr' or for AgentR-mediated integrations.",
118
+ alias="AGENTR_BASE_URL",
119
+ )
120
+ api_key: SecretStr | None = Field(
121
+ default=None,
122
+ description="The API key for authenticating with the AgentR platform. Stored as a SecretStr for security.",
123
+ alias="AGENTR_API_KEY",
124
+ )
125
+ # Local specific settings
126
+ apps: list[AppConfig] | None = Field(
127
+ default=None,
128
+ description="A list of application configurations to load when server 'type' is 'local'. Ignored if 'type' is 'agentr'.",
129
+ )
130
+ store: StoreConfig | None = Field(
131
+ default=None,
132
+ description="Default credential store configuration for applications that do not define their own specific store.",
57
133
  )
58
- port: int = Field(default=8005, description="Port to run the server on (if applicable)", ge=1024, le=65535)
59
- host: str = Field(default="localhost", description="Host to bind the server to (if applicable)")
60
- apps: list[AppConfig] | None = Field(default=None, description="List of configured applications")
61
- store: StoreConfig | None = Field(default=None, description="Default store configuration")
62
- debug: bool = Field(default=False, description="Enable debug mode")
63
- log_level: str = Field(default="INFO", description="Logging level")
64
134
 
65
135
  @field_validator("log_level", mode="before")
66
136
  def validate_log_level(cls, v: str) -> str:
137
+ """Validates and normalizes the log_level field."""
67
138
  valid_levels = ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]
68
139
  if v.upper() not in valid_levels:
69
140
  raise ValueError(f"Invalid log level. Must be one of: {', '.join(valid_levels)}")
@@ -71,32 +142,59 @@ class ServerConfig(BaseSettings):
71
142
 
72
143
  @field_validator("port", mode="before")
73
144
  def validate_port(cls, v: int) -> int:
145
+ """Validates the port number is within the valid range."""
74
146
  if not 1 <= v <= 65535:
75
147
  raise ValueError("Port must be between 1 and 65535")
76
148
  return v
77
149
 
78
150
  @classmethod
79
151
  def load_json_config(cls, path: str = "local_config.json") -> Self:
152
+ """Loads server configuration from a JSON file.
153
+
154
+ Args:
155
+ path (str, optional): The path to the JSON configuration file.
156
+ Defaults to "local_config.json".
157
+
158
+ Returns:
159
+ ServerConfig: An instance of ServerConfig populated with data
160
+ from the JSON file.
161
+ """
80
162
  with open(path) as f:
81
163
  data = json.load(f)
82
164
  return cls.model_validate(data)
83
165
 
84
166
 
85
167
  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] = {}
168
+ """Configuration for how an MCP client connects to an MCP server.
169
+
170
+ Specifies the transport protocol and its associated parameters, such as
171
+ the command for stdio, URL for HTTP-based transports (SSE, streamable_http),
172
+ and any necessary headers or environment variables.
173
+ """
174
+
175
+ transport: str | None = Field(
176
+ default=None,
177
+ description="The transport protocol (e.g., 'stdio', 'sse', 'streamable_http'). Auto-detected in model_validate if not set.",
178
+ )
179
+ command: str | None = Field(
180
+ default=None, description="The command to execute for 'stdio' transport (e.g., 'python -m mcp_server.run')."
181
+ )
182
+ args: list[str] = Field(default=[], description="List of arguments for the 'stdio' command.")
183
+ env: dict[str, str] = Field(default={}, description="Environment variables to set for the 'stdio' command.")
184
+ url: str | None = Field(default=None, description="The URL for 'sse' or 'streamable_http' transport.")
185
+ headers: dict[str, str] = Field(
186
+ default={}, description="HTTP headers to include for 'sse' or 'streamable_http' transport."
187
+ )
92
188
 
93
189
  @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'.
190
+ def determine_transport_if_not_set(self) -> Self:
191
+ """Determines and sets the transport type if not explicitly provided.
192
+
193
+ - If `command` is present, transport is set to 'stdio'.
194
+ - If `url` is present, transport is 'streamable_http' if URL ends with '/mcp',
195
+ otherwise 'sse' if URL ends with '/sse'.
196
+ - Raises ValueError if transport cannot be determined or if neither
197
+ `command` nor `url` is provided.
100
198
  """
101
199
  if self.command:
102
200
  self.transport = "stdio"
@@ -114,18 +212,34 @@ class ClientTransportConfig(BaseModel):
114
212
  return self
115
213
 
116
214
 
117
- class LLMConfig(BaseModel):
118
- api_key: str
119
- base_url: str
120
- model: str
215
+ class ClientConfig(BaseSettings):
216
+ """Configuration for a client application that interacts with MCP servers and an LLM.
121
217
 
218
+ Defines connections to one or more MCP servers (via `mcpServers`) and
219
+ optionally, settings for an LLM to be used by the client (e.g., by an agent).
220
+ """
122
221
 
123
- class ClientConfig(BaseSettings):
124
- mcpServers: dict[str, ClientTransportConfig]
125
- llm: LLMConfig | None = None
222
+ mcpServers: dict[str, ClientTransportConfig] = Field(
223
+ ...,
224
+ description="A dictionary where keys are descriptive names for MCP server connections and values are `ClientTransportConfig` objects defining how to connect to each server.",
225
+ )
126
226
 
127
227
  @classmethod
128
228
  def load_json_config(cls, path: str = "servers.json") -> Self:
229
+ """Loads client configuration from a JSON file.
230
+
231
+ Args:
232
+ path (str, optional): The path to the JSON configuration file.
233
+ Defaults to "servers.json".
234
+
235
+ Returns:
236
+ ClientConfig: An instance of ClientConfig populated with data
237
+ from the JSON file.
238
+ """
129
239
  with open(path) as f:
130
240
  data = json.load(f)
131
241
  return cls.model_validate(data)
242
+
243
+ def save_json_config(self, path: str) -> None:
244
+ with open(path, "w") as f:
245
+ json.dump(self.model_dump(), f, indent=4)
@@ -1,25 +1,69 @@
1
1
  class NotAuthorizedError(Exception):
2
- """Raised when a user is not authorized to access a resource or perform an action."""
2
+ """Raised when an action is attempted without necessary permissions.
3
+
4
+ This typically occurs if a user or process tries to access a protected
5
+ resource or perform an operation for which they lack the required
6
+ authorization credentials or roles.
7
+ """
3
8
 
4
9
  def __init__(self, message: str):
10
+ """Initializes the NotAuthorizedError.
11
+
12
+ Args:
13
+ message (str): A descriptive message explaining the authorization failure.
14
+ """
5
15
  self.message = message
16
+ super().__init__(message) # Ensure message is passed to base Exception
6
17
 
7
18
 
8
19
  class ToolError(Exception):
9
- """Raised when a tool is not found or fails to execute."""
20
+ """Indicates an issue related to tool discovery, validation, or execution.
21
+
22
+ This could be due to a tool not being found, failing during its
23
+ operation, or having invalid configuration or arguments.
24
+ """
25
+
26
+ pass
10
27
 
11
28
 
12
29
  class InvalidSignature(Exception):
13
- """Raised when a signature is invalid."""
30
+ """Raised when a cryptographic signature verification fails.
31
+
32
+ This can occur during webhook validation or any other process that
33
+ relies on verifying the authenticity and integrity of a message
34
+ using a digital signature.
35
+ """
36
+
37
+ pass
14
38
 
15
39
 
16
40
  class StoreError(Exception):
17
- """Base exception class for store-related errors."""
41
+ """Base exception for errors related to data or credential stores.
42
+
43
+ This serves as a generic error for issues arising from operations
44
+ on any storage backend (e.g., KeyringStore, EnvironmentStore).
45
+ Specific store errors should ideally subclass this.
46
+ """
47
+
48
+ pass
18
49
 
19
50
 
20
51
  class KeyNotFoundError(StoreError):
21
- """Exception raised when a key is not found in the store."""
52
+ """Raised when a specified key cannot be found in a data or credential store.
53
+
54
+ This is a common error when attempting to retrieve a piece of data
55
+ (e.g., an API key, token, or client information) that does not exist
56
+ under the given identifier.
57
+ """
58
+
59
+ pass
22
60
 
23
61
 
24
62
  class ConfigurationError(Exception):
25
- """Exception raised when a configuration error occurs."""
63
+ """Indicates an error was detected in application or server configuration.
64
+
65
+ This can be due to missing required settings, invalid values for
66
+ configuration parameters, or inconsistencies in the provided setup.
67
+ """
68
+
69
+ pass