mcp-use 1.3.7__py3-none-any.whl → 1.3.9__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.

Potentially problematic release.


This version of mcp-use might be problematic. Click here for more details.

@@ -0,0 +1,296 @@
1
+ """
2
+ Remote agent implementation for executing agents via API.
3
+ """
4
+
5
+ import json
6
+ import os
7
+ from typing import Any, TypeVar
8
+
9
+ import httpx
10
+ from langchain.schema import BaseMessage
11
+ from pydantic import BaseModel
12
+
13
+ from ..logging import logger
14
+
15
+ T = TypeVar("T", bound=BaseModel)
16
+
17
+ # API endpoint constants
18
+ API_CHATS_ENDPOINT = "/api/v1/chats"
19
+ API_CHAT_EXECUTE_ENDPOINT = "/api/v1/chats/{chat_id}/execute"
20
+ API_CHAT_DELETE_ENDPOINT = "/api/v1/chats/{chat_id}"
21
+
22
+
23
+ class RemoteAgent:
24
+ """Agent that executes remotely via API."""
25
+
26
+ def __init__(self, agent_id: str, api_key: str | None = None, base_url: str = "https://cloud.mcp-use.com"):
27
+ """Initialize remote agent.
28
+
29
+ Args:
30
+ agent_id: The ID of the remote agent to execute
31
+ api_key: API key for authentication. If None, will check MCP_USE_API_KEY env var
32
+ base_url: Base URL for the remote API
33
+ """
34
+ self.agent_id = agent_id
35
+ self.base_url = base_url
36
+ self._chat_id = None # Persistent chat session
37
+
38
+ # Handle API key validation
39
+ if api_key is None:
40
+ api_key = os.getenv("MCP_USE_API_KEY")
41
+ if not api_key:
42
+ raise ValueError(
43
+ "API key is required for remote execution. "
44
+ "Please provide it as a parameter or set the MCP_USE_API_KEY environment variable. "
45
+ "You can get an API key from https://cloud.mcp-use.com"
46
+ )
47
+
48
+ self.api_key = api_key
49
+ # Configure client with reasonable timeouts for agent execution
50
+ self._client = httpx.AsyncClient(
51
+ timeout=httpx.Timeout(
52
+ connect=10.0, # 10 seconds to establish connection
53
+ read=300.0, # 5 minutes to read response (agents can take time)
54
+ write=10.0, # 10 seconds to send request
55
+ pool=10.0, # 10 seconds to get connection from pool
56
+ )
57
+ )
58
+
59
+ def _pydantic_to_json_schema(self, model_class: type[T]) -> dict[str, Any]:
60
+ """Convert a Pydantic model to JSON schema for API transmission.
61
+
62
+ Args:
63
+ model_class: The Pydantic model class to convert
64
+
65
+ Returns:
66
+ JSON schema representation of the model
67
+ """
68
+ return model_class.model_json_schema()
69
+
70
+ def _parse_structured_response(self, response_data: Any, output_schema: type[T]) -> T:
71
+ """Parse the API response into the structured output format.
72
+
73
+ Args:
74
+ response_data: Raw response data from the API
75
+ output_schema: The Pydantic model to parse into
76
+
77
+ Returns:
78
+ Parsed structured output
79
+ """
80
+ # Handle different response formats
81
+ if isinstance(response_data, dict):
82
+ if "result" in response_data:
83
+ outer_result = response_data["result"]
84
+ # Check if this is a nested result structure (agent execution response)
85
+ if isinstance(outer_result, dict) and "result" in outer_result:
86
+ # Extract the actual structured output from the nested result
87
+ result_data = outer_result["result"]
88
+ else:
89
+ # Use the outer result directly
90
+ result_data = outer_result
91
+ else:
92
+ result_data = response_data
93
+ elif isinstance(response_data, str):
94
+ try:
95
+ result_data = json.loads(response_data)
96
+ except json.JSONDecodeError:
97
+ # If it's not valid JSON, try to create the model from the string content
98
+ result_data = {"content": response_data}
99
+ else:
100
+ result_data = response_data
101
+
102
+ # Parse into the Pydantic model
103
+ try:
104
+ return output_schema.model_validate(result_data)
105
+ except Exception as e:
106
+ logger.warning(f"Failed to parse structured output: {e}")
107
+ # Fallback: try to parse it as raw content if the model has a content field
108
+ if hasattr(output_schema, "model_fields") and "content" in output_schema.model_fields:
109
+ return output_schema.model_validate({"content": str(result_data)})
110
+ raise
111
+
112
+ async def _create_chat_session(self, query: str) -> str:
113
+ """Create a persistent chat session for the agent.
114
+ Args:
115
+ query: The initial query (not used in title anymore)
116
+ Returns:
117
+ The chat ID of the created session
118
+ Raises:
119
+ RuntimeError: If chat creation fails
120
+ """
121
+ chat_payload = {
122
+ "title": f"Remote Agent Session - {self.agent_id}",
123
+ "agent_id": self.agent_id,
124
+ "type": "agent_execution",
125
+ }
126
+
127
+ headers = {"Content-Type": "application/json", "x-api-key": self.api_key}
128
+ chat_url = f"{self.base_url}{API_CHATS_ENDPOINT}"
129
+
130
+ logger.info(f"📝 Creating chat session for agent {self.agent_id}")
131
+
132
+ try:
133
+ chat_response = await self._client.post(chat_url, json=chat_payload, headers=headers)
134
+ chat_response.raise_for_status()
135
+
136
+ chat_data = chat_response.json()
137
+ chat_id = chat_data["id"]
138
+ logger.info(f"✅ Chat session created: {chat_id}")
139
+ return chat_id
140
+
141
+ except httpx.HTTPStatusError as e:
142
+ status_code = e.response.status_code
143
+ response_text = e.response.text
144
+
145
+ if status_code == 404:
146
+ raise RuntimeError(
147
+ f"Agent not found: Agent '{self.agent_id}' does not exist or you don't have access to it. "
148
+ "Please verify the agent ID and ensure it exists in your account."
149
+ ) from e
150
+ else:
151
+ raise RuntimeError(f"Failed to create chat session: {status_code} - {response_text}") from e
152
+ except Exception as e:
153
+ raise RuntimeError(f"Failed to create chat session: {str(e)}") from e
154
+
155
+ async def run(
156
+ self,
157
+ query: str,
158
+ max_steps: int | None = None,
159
+ manage_connector: bool = True,
160
+ external_history: list[BaseMessage] | None = None,
161
+ output_schema: type[T] | None = None,
162
+ ) -> str | T:
163
+ """Run a query on the remote agent.
164
+
165
+ Args:
166
+ query: The query to execute
167
+ max_steps: Maximum number of steps (default: 10)
168
+ manage_connector: Ignored for remote execution
169
+ external_history: Ignored for remote execution (not supported yet)
170
+ output_schema: Optional Pydantic model for structured output
171
+
172
+ Returns:
173
+ The result from the remote agent execution (string or structured output)
174
+ """
175
+ if external_history is not None:
176
+ logger.warning("External history is not yet supported for remote execution")
177
+
178
+ try:
179
+ logger.info(f"🌐 Executing query on remote agent {self.agent_id}")
180
+
181
+ # Step 1: Create a chat session for this agent (only if we don't have one)
182
+ if self._chat_id is None:
183
+ self._chat_id = await self._create_chat_session(query)
184
+
185
+ chat_id = self._chat_id
186
+
187
+ # Step 2: Execute the agent within the chat context
188
+ execution_payload = {"query": query, "max_steps": max_steps or 10}
189
+
190
+ # Add structured output schema if provided
191
+ if output_schema is not None:
192
+ execution_payload["output_schema"] = self._pydantic_to_json_schema(output_schema)
193
+ logger.info(f"🔧 Using structured output with schema: {output_schema.__name__}")
194
+
195
+ headers = {"Content-Type": "application/json", "x-api-key": self.api_key}
196
+ execution_url = f"{self.base_url}{API_CHAT_EXECUTE_ENDPOINT.format(chat_id=chat_id)}"
197
+ logger.info(f"🚀 Executing agent in chat {chat_id}")
198
+
199
+ response = await self._client.post(execution_url, json=execution_payload, headers=headers)
200
+ response.raise_for_status()
201
+
202
+ result = response.json()
203
+ logger.info(f"🔧 Response: {result}")
204
+ logger.info("✅ Remote execution completed successfully")
205
+
206
+ # Check for error responses (even with 200 status)
207
+ if isinstance(result, dict):
208
+ # Check for actual error conditions (not just presence of error field)
209
+ if result.get("status") == "error" or (result.get("error") is not None):
210
+ error_msg = result.get("error", str(result))
211
+ logger.error(f"❌ Remote agent execution failed: {error_msg}")
212
+ raise RuntimeError(f"Remote agent execution failed: {error_msg}")
213
+
214
+ # Check if the response indicates agent initialization failure
215
+ if "failed to initialize" in str(result):
216
+ logger.error(f"❌ Agent initialization failed: {result}")
217
+ raise RuntimeError(
218
+ f"Agent initialization failed on remote server. "
219
+ f"This usually indicates:\n"
220
+ f"• Invalid agent configuration (LLM model, system prompt)\n"
221
+ f"• Missing or invalid MCP server configurations\n"
222
+ f"• Network connectivity issues with MCP servers\n"
223
+ f"• Missing environment variables or credentials\n"
224
+ f"Raw error: {result}"
225
+ )
226
+
227
+ # Handle structured output
228
+ if output_schema is not None:
229
+ return self._parse_structured_response(result, output_schema)
230
+
231
+ # Regular string output
232
+ if isinstance(result, dict) and "result" in result:
233
+ return result["result"]
234
+ elif isinstance(result, str):
235
+ return result
236
+ else:
237
+ return str(result)
238
+
239
+ except httpx.HTTPStatusError as e:
240
+ status_code = e.response.status_code
241
+ response_text = e.response.text
242
+
243
+ # Provide specific error messages based on status code
244
+ if status_code == 401:
245
+ logger.error(f"❌ Authentication failed: {response_text}")
246
+ raise RuntimeError(
247
+ "Authentication failed: Invalid or missing API key. "
248
+ "Please check your API key and ensure the MCP_USE_API_KEY environment variable is set correctly."
249
+ ) from e
250
+ elif status_code == 403:
251
+ logger.error(f"❌ Access forbidden: {response_text}")
252
+ raise RuntimeError(
253
+ f"Access denied: You don't have permission to execute agent '{self.agent_id}'. "
254
+ "Check if the agent exists and you have the necessary permissions."
255
+ ) from e
256
+ elif status_code == 404:
257
+ logger.error(f"❌ Agent not found: {response_text}")
258
+ raise RuntimeError(
259
+ f"Agent not found: Agent '{self.agent_id}' does not exist or you don't have access to it. "
260
+ "Please verify the agent ID and ensure it exists in your account."
261
+ ) from e
262
+ elif status_code == 422:
263
+ logger.error(f"❌ Validation error: {response_text}")
264
+ raise RuntimeError(
265
+ f"Request validation failed: {response_text}. "
266
+ "Please check your query parameters and output schema format."
267
+ ) from e
268
+ elif status_code == 500:
269
+ logger.error(f"❌ Server error: {response_text}")
270
+ raise RuntimeError(
271
+ "Internal server error occurred during agent execution. "
272
+ "Please try again later or contact support if the issue persists."
273
+ ) from e
274
+ else:
275
+ logger.error(f"❌ Remote execution failed with status {status_code}: {response_text}")
276
+ raise RuntimeError(f"Remote agent execution failed: {status_code} - {response_text}") from e
277
+ except httpx.TimeoutException as e:
278
+ logger.error(f"❌ Remote execution timed out: {e}")
279
+ raise RuntimeError(
280
+ "Remote agent execution timed out. The server may be overloaded or the query is taking too long to "
281
+ "process. Try again or use a simpler query."
282
+ ) from e
283
+ except httpx.ConnectError as e:
284
+ logger.error(f"❌ Remote execution connection error: {e}")
285
+ raise RuntimeError(
286
+ f"Remote agent connection failed: Cannot connect to {self.base_url}. "
287
+ f"Check if the server is running and the URL is correct."
288
+ ) from e
289
+ except Exception as e:
290
+ logger.error(f"❌ Remote execution error: {e}")
291
+ raise RuntimeError(f"Remote agent execution failed: {str(e)}") from e
292
+
293
+ async def close(self) -> None:
294
+ """Close the HTTP client."""
295
+ await self._client.aclose()
296
+ logger.info("🔌 Remote agent client closed")
mcp_use/client.py CHANGED
@@ -9,7 +9,7 @@ import json
9
9
  import warnings
10
10
  from typing import Any
11
11
 
12
- from mcp.client.session import ElicitationFnT, SamplingFnT
12
+ from mcp.client.session import ElicitationFnT, LoggingFnT, MessageHandlerFnT, SamplingFnT
13
13
 
14
14
  from mcp_use.types.sandbox import SandboxOptions
15
15
 
@@ -28,10 +28,13 @@ class MCPClient:
28
28
  def __init__(
29
29
  self,
30
30
  config: str | dict[str, Any] | None = None,
31
+ allowed_servers: list[str] | None = None,
31
32
  sandbox: bool = False,
32
33
  sandbox_options: SandboxOptions | None = None,
33
34
  sampling_callback: SamplingFnT | None = None,
34
35
  elicitation_callback: ElicitationFnT | None = None,
36
+ message_handler: MessageHandlerFnT | None = None,
37
+ logging_callback: LoggingFnT | None = None,
35
38
  ) -> None:
36
39
  """Initialize a new MCP client.
37
40
 
@@ -43,12 +46,15 @@ class MCPClient:
43
46
  sampling_callback: Optional sampling callback function.
44
47
  """
45
48
  self.config: dict[str, Any] = {}
49
+ self.allowed_servers: list[str] = allowed_servers
46
50
  self.sandbox = sandbox
47
51
  self.sandbox_options = sandbox_options
48
52
  self.sessions: dict[str, MCPSession] = {}
49
53
  self.active_sessions: list[str] = []
50
54
  self.sampling_callback = sampling_callback
51
55
  self.elicitation_callback = elicitation_callback
56
+ self.message_handler = message_handler
57
+ self.logging_callback = logging_callback
52
58
  # Load configuration if provided
53
59
  if config is not None:
54
60
  if isinstance(config, str):
@@ -64,6 +70,8 @@ class MCPClient:
64
70
  sandbox_options: SandboxOptions | None = None,
65
71
  sampling_callback: SamplingFnT | None = None,
66
72
  elicitation_callback: ElicitationFnT | None = None,
73
+ message_handler: MessageHandlerFnT | None = None,
74
+ logging_callback: LoggingFnT | None = None,
67
75
  ) -> "MCPClient":
68
76
  """Create a MCPClient from a dictionary.
69
77
 
@@ -80,6 +88,8 @@ class MCPClient:
80
88
  sandbox_options=sandbox_options,
81
89
  sampling_callback=sampling_callback,
82
90
  elicitation_callback=elicitation_callback,
91
+ message_handler=message_handler,
92
+ logging_callback=logging_callback,
83
93
  )
84
94
 
85
95
  @classmethod
@@ -90,6 +100,8 @@ class MCPClient:
90
100
  sandbox_options: SandboxOptions | None = None,
91
101
  sampling_callback: SamplingFnT | None = None,
92
102
  elicitation_callback: ElicitationFnT | None = None,
103
+ message_handler: MessageHandlerFnT | None = None,
104
+ logging_callback: LoggingFnT | None = None,
93
105
  ) -> "MCPClient":
94
106
  """Create a MCPClient from a configuration file.
95
107
 
@@ -106,6 +118,8 @@ class MCPClient:
106
118
  sandbox_options=sandbox_options,
107
119
  sampling_callback=sampling_callback,
108
120
  elicitation_callback=elicitation_callback,
121
+ message_handler=message_handler,
122
+ logging_callback=logging_callback,
109
123
  )
110
124
 
111
125
  def add_server(
@@ -185,6 +199,8 @@ class MCPClient:
185
199
  sandbox_options=self.sandbox_options,
186
200
  sampling_callback=self.sampling_callback,
187
201
  elicitation_callback=self.elicitation_callback,
202
+ message_handler=self.message_handler,
203
+ logging_callback=self.logging_callback,
188
204
  )
189
205
 
190
206
  # Create the session
@@ -220,9 +236,10 @@ class MCPClient:
220
236
  warnings.warn("No MCP servers defined in config", UserWarning, stacklevel=2)
221
237
  return {}
222
238
 
223
- # Create sessions for all servers
239
+ # Create sessions only for allowed servers if applicable else create for all servers
224
240
  for name in servers:
225
- await self.create_session(name, auto_initialize)
241
+ if self.allowed_servers is None or name in self.allowed_servers:
242
+ await self.create_session(name, auto_initialize)
226
243
 
227
244
  return self.sessions
228
245
 
mcp_use/config.py CHANGED
@@ -7,17 +7,11 @@ This module provides functionality to load MCP configuration from JSON files.
7
7
  import json
8
8
  from typing import Any
9
9
 
10
- from mcp.client.session import ElicitationFnT, SamplingFnT
10
+ from mcp.client.session import ElicitationFnT, LoggingFnT, MessageHandlerFnT, SamplingFnT
11
11
 
12
12
  from mcp_use.types.sandbox import SandboxOptions
13
13
 
14
- from .connectors import (
15
- BaseConnector,
16
- HttpConnector,
17
- SandboxConnector,
18
- StdioConnector,
19
- WebSocketConnector,
20
- )
14
+ from .connectors import BaseConnector, HttpConnector, SandboxConnector, StdioConnector, WebSocketConnector
21
15
  from .connectors.utils import is_stdio_server
22
16
 
23
17
 
@@ -40,6 +34,8 @@ def create_connector_from_config(
40
34
  sandbox_options: SandboxOptions | None = None,
41
35
  sampling_callback: SamplingFnT | None = None,
42
36
  elicitation_callback: ElicitationFnT | None = None,
37
+ message_handler: MessageHandlerFnT | None = None,
38
+ logging_callback: LoggingFnT | None = None,
43
39
  ) -> BaseConnector:
44
40
  """Create a connector based on server configuration.
45
41
  This function can be called with just the server_config parameter:
@@ -61,6 +57,8 @@ def create_connector_from_config(
61
57
  env=server_config.get("env", None),
62
58
  sampling_callback=sampling_callback,
63
59
  elicitation_callback=elicitation_callback,
60
+ message_handler=message_handler,
61
+ logging_callback=logging_callback,
64
62
  )
65
63
 
66
64
  # Sandboxed connector
@@ -72,6 +70,8 @@ def create_connector_from_config(
72
70
  e2b_options=sandbox_options,
73
71
  sampling_callback=sampling_callback,
74
72
  elicitation_callback=elicitation_callback,
73
+ message_handler=message_handler,
74
+ logging_callback=logging_callback,
75
75
  )
76
76
 
77
77
  # HTTP connector
@@ -80,8 +80,12 @@ def create_connector_from_config(
80
80
  base_url=server_config["url"],
81
81
  headers=server_config.get("headers", None),
82
82
  auth_token=server_config.get("auth_token", None),
83
+ timeout=server_config.get("timeout", 5),
84
+ sse_read_timeout=server_config.get("sse_read_timeout", 60 * 5),
83
85
  sampling_callback=sampling_callback,
84
86
  elicitation_callback=elicitation_callback,
87
+ message_handler=message_handler,
88
+ logging_callback=logging_callback,
85
89
  )
86
90
 
87
91
  # WebSocket connector
@@ -5,14 +5,27 @@ This module provides the base connector interface that all MCP connectors
5
5
  must implement.
6
6
  """
7
7
 
8
+ import warnings
8
9
  from abc import ABC, abstractmethod
9
10
  from datetime import timedelta
10
11
  from typing import Any
11
12
 
12
13
  from mcp import ClientSession, Implementation
13
- from mcp.client.session import ElicitationFnT, SamplingFnT
14
+ from mcp.client.session import ElicitationFnT, LoggingFnT, MessageHandlerFnT, SamplingFnT
14
15
  from mcp.shared.exceptions import McpError
15
- from mcp.types import CallToolResult, GetPromptResult, Prompt, ReadResourceResult, Resource, Tool
16
+ from mcp.types import (
17
+ CallToolResult,
18
+ GetPromptResult,
19
+ Prompt,
20
+ PromptListChangedNotification,
21
+ ReadResourceResult,
22
+ Resource,
23
+ ResourceListChangedNotification,
24
+ ServerCapabilities,
25
+ ServerNotification,
26
+ Tool,
27
+ ToolListChangedNotification,
28
+ )
16
29
  from pydantic import AnyUrl
17
30
 
18
31
  import mcp_use
@@ -31,6 +44,8 @@ class BaseConnector(ABC):
31
44
  self,
32
45
  sampling_callback: SamplingFnT | None = None,
33
46
  elicitation_callback: ElicitationFnT | None = None,
47
+ message_handler: MessageHandlerFnT | None = None,
48
+ logging_callback: LoggingFnT | None = None,
34
49
  ):
35
50
  """Initialize base connector with common attributes."""
36
51
  self.client_session: ClientSession | None = None
@@ -43,6 +58,9 @@ class BaseConnector(ABC):
43
58
  self.auto_reconnect = True # Whether to automatically reconnect on connection loss (not configurable for now)
44
59
  self.sampling_callback = sampling_callback
45
60
  self.elicitation_callback = elicitation_callback
61
+ self.message_handler = message_handler
62
+ self.logging_callback = logging_callback
63
+ self.capabilities: ServerCapabilities | None = None
46
64
 
47
65
  @property
48
66
  def client_info(self) -> Implementation:
@@ -53,6 +71,20 @@ class BaseConnector(ABC):
53
71
  url="https://github.com/mcp-use/mcp-use",
54
72
  )
55
73
 
74
+ async def _internal_message_handler(self, message: Any) -> None:
75
+ """Wrap the user-provided message handler."""
76
+ if isinstance(message, ServerNotification):
77
+ if isinstance(message.root, ToolListChangedNotification):
78
+ logger.debug("Received tool list changed notification")
79
+ elif isinstance(message.root, ResourceListChangedNotification):
80
+ logger.debug("Received resource list changed notification")
81
+ elif isinstance(message.root, PromptListChangedNotification):
82
+ logger.debug("Received prompt list changed notification")
83
+
84
+ # Call the user's handler
85
+ if self.message_handler:
86
+ await self.message_handler(message)
87
+
56
88
  @abstractmethod
57
89
  async def connect(self) -> None:
58
90
  """Establish a connection to the MCP implementation."""
@@ -125,37 +157,37 @@ class BaseConnector(ABC):
125
157
  result = await self.client_session.initialize()
126
158
  self._initialized = True # Mark as initialized
127
159
 
128
- server_capabilities = result.capabilities
160
+ self.capabilities = result.capabilities
129
161
 
130
- if server_capabilities.tools:
162
+ if self.capabilities.tools:
131
163
  # Get available tools directly from client session
132
164
  try:
133
165
  tools_result = await self.client_session.list_tools()
134
166
  self._tools = tools_result.tools if tools_result else []
135
167
  except Exception as e:
136
- logger.error(f"Error listing tools: {e}")
168
+ logger.error(f"Error listing tools for connector {self.public_identifier}: {e}")
137
169
  self._tools = []
138
170
  else:
139
171
  self._tools = []
140
172
 
141
- if server_capabilities.resources:
173
+ if self.capabilities.resources:
142
174
  # Get available resources directly from client session
143
175
  try:
144
176
  resources_result = await self.client_session.list_resources()
145
177
  self._resources = resources_result.resources if resources_result else []
146
178
  except Exception as e:
147
- logger.error(f"Error listing resources: {e}")
179
+ logger.error(f"Error listing resources for connector {self.public_identifier}: {e}")
148
180
  self._resources = []
149
181
  else:
150
182
  self._resources = []
151
183
 
152
- if server_capabilities.prompts:
184
+ if self.capabilities.prompts:
153
185
  # Get available prompts directly from client session
154
186
  try:
155
187
  prompts_result = await self.client_session.list_prompts()
156
188
  self._prompts = prompts_result.prompts if prompts_result else []
157
189
  except Exception as e:
158
- logger.error(f"Error listing prompts: {e}")
190
+ logger.error(f"Error listing prompts for connector {self.public_identifier}: {e}")
159
191
  self._prompts = []
160
192
  else:
161
193
  self._prompts = []
@@ -170,21 +202,57 @@ class BaseConnector(ABC):
170
202
 
171
203
  @property
172
204
  def tools(self) -> list[Tool]:
173
- """Get the list of available tools."""
205
+ """Get the list of available tools.
206
+
207
+ .. deprecated::
208
+ This property is deprecated because it may return stale data when the server
209
+ sends list change notifications. Use `await list_tools()` instead to ensure
210
+ you always get the latest data.
211
+ """
212
+ warnings.warn(
213
+ "The 'tools' property is deprecated and may return stale data. "
214
+ "Use 'await list_tools()' instead to ensure fresh data.",
215
+ DeprecationWarning,
216
+ stacklevel=2,
217
+ )
174
218
  if self._tools is None:
175
219
  raise RuntimeError("MCP client is not initialized")
176
220
  return self._tools
177
221
 
178
222
  @property
179
223
  def resources(self) -> list[Resource]:
180
- """Get the list of available resources."""
224
+ """Get the list of available resources.
225
+
226
+ .. deprecated::
227
+ This property is deprecated because it may return stale data when the server
228
+ sends list change notifications. Use `await list_resources()` instead to ensure
229
+ you always get the latest data.
230
+ """
231
+ warnings.warn(
232
+ "The 'resources' property is deprecated and may return stale data. "
233
+ "Use 'await list_resources()' instead to ensure fresh data.",
234
+ DeprecationWarning,
235
+ stacklevel=2,
236
+ )
181
237
  if self._resources is None:
182
238
  raise RuntimeError("MCP client is not initialized")
183
239
  return self._resources
184
240
 
185
241
  @property
186
242
  def prompts(self) -> list[Prompt]:
187
- """Get the list of available prompts."""
243
+ """Get the list of available prompts.
244
+
245
+ .. deprecated::
246
+ This property is deprecated because it may return stale data when the server
247
+ sends list change notifications. Use `await list_prompts()' instead to ensure
248
+ you always get the latest data.
249
+ """
250
+ warnings.warn(
251
+ "The 'prompts' property is deprecated and may return stale data. "
252
+ "Use 'await list_prompts()' instead to ensure fresh data.",
253
+ DeprecationWarning,
254
+ stacklevel=2,
255
+ )
188
256
  if self._prompts is None:
189
257
  raise RuntimeError("MCP client is not initialized")
190
258
  return self._prompts
@@ -303,28 +371,39 @@ class BaseConnector(ABC):
303
371
  async def list_tools(self) -> list[Tool]:
304
372
  """List all available tools from the MCP implementation."""
305
373
 
374
+ if self.capabilities and not self.capabilities.tools:
375
+ logger.debug(f"Server {self.public_identifier} does not support tools")
376
+ return []
377
+
306
378
  # Ensure we're connected
307
379
  await self._ensure_connected()
308
380
 
309
381
  logger.debug("Listing tools")
310
382
  try:
311
383
  result = await self.client_session.list_tools()
384
+ self._tools = result.tools
312
385
  return result.tools
313
386
  except McpError as e:
314
- logger.error(f"Error listing tools: {e}")
387
+ logger.error(f"Error listing tools for connector {self.public_identifier}: {e}")
315
388
  return []
316
389
 
317
390
  async def list_resources(self) -> list[Resource]:
318
391
  """List all available resources from the MCP implementation."""
392
+
393
+ if self.capabilities and not self.capabilities.resources:
394
+ logger.debug(f"Server {self.public_identifier} does not support resources")
395
+ return []
396
+
319
397
  # Ensure we're connected
320
398
  await self._ensure_connected()
321
399
 
322
400
  logger.debug("Listing resources")
323
401
  try:
324
402
  result = await self.client_session.list_resources()
403
+ self._resources = result.resources
325
404
  return result.resources
326
405
  except McpError as e:
327
- logger.error(f"Error listing resources: {e}")
406
+ logger.warning(f"Error listing resources for connector {self.public_identifier}: {e}")
328
407
  return []
329
408
 
330
409
  async def read_resource(self, uri: AnyUrl) -> ReadResourceResult:
@@ -337,14 +416,20 @@ class BaseConnector(ABC):
337
416
 
338
417
  async def list_prompts(self) -> list[Prompt]:
339
418
  """List all available prompts from the MCP implementation."""
419
+
420
+ if self.capabilities and not self.capabilities.prompts:
421
+ logger.debug(f"Server {self.public_identifier} does not support prompts")
422
+ return []
423
+
340
424
  await self._ensure_connected()
341
425
 
342
426
  logger.debug("Listing prompts")
343
427
  try:
344
428
  result = await self.client_session.list_prompts()
429
+ self._prompts = result.prompts
345
430
  return result.prompts
346
431
  except McpError as e:
347
- logger.error(f"Error listing prompts: {e}")
432
+ logger.error(f"Error listing prompts for connector {self.public_identifier}: {e}")
348
433
  return []
349
434
 
350
435
  async def get_prompt(self, name: str, arguments: dict[str, Any] | None = None) -> GetPromptResult: