mcp-use 1.3.11__py3-none-any.whl → 1.3.13__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.
- mcp_use/__init__.py +1 -1
- mcp_use/adapters/.deprecated +0 -0
- mcp_use/adapters/__init__.py +18 -7
- mcp_use/adapters/base.py +12 -185
- mcp_use/adapters/langchain_adapter.py +12 -264
- mcp_use/agents/adapters/__init__.py +10 -0
- mcp_use/agents/adapters/base.py +193 -0
- mcp_use/agents/adapters/langchain_adapter.py +228 -0
- mcp_use/agents/base.py +1 -1
- mcp_use/agents/managers/__init__.py +19 -0
- mcp_use/agents/managers/base.py +36 -0
- mcp_use/agents/managers/server_manager.py +131 -0
- mcp_use/agents/managers/tools/__init__.py +15 -0
- mcp_use/agents/managers/tools/base_tool.py +19 -0
- mcp_use/agents/managers/tools/connect_server.py +69 -0
- mcp_use/agents/managers/tools/disconnect_server.py +43 -0
- mcp_use/agents/managers/tools/get_active_server.py +29 -0
- mcp_use/agents/managers/tools/list_servers_tool.py +53 -0
- mcp_use/agents/managers/tools/search_tools.py +328 -0
- mcp_use/agents/mcpagent.py +88 -47
- mcp_use/agents/remote.py +168 -129
- mcp_use/auth/.deprecated +0 -0
- mcp_use/auth/__init__.py +19 -4
- mcp_use/auth/bearer.py +11 -12
- mcp_use/auth/oauth.py +11 -620
- mcp_use/auth/oauth_callback.py +16 -207
- mcp_use/client/__init__.py +1 -0
- mcp_use/client/auth/__init__.py +6 -0
- mcp_use/client/auth/bearer.py +23 -0
- mcp_use/client/auth/oauth.py +629 -0
- mcp_use/client/auth/oauth_callback.py +214 -0
- mcp_use/client/client.py +356 -0
- mcp_use/client/config.py +106 -0
- mcp_use/client/connectors/__init__.py +20 -0
- mcp_use/client/connectors/base.py +470 -0
- mcp_use/client/connectors/http.py +304 -0
- mcp_use/client/connectors/sandbox.py +332 -0
- mcp_use/client/connectors/stdio.py +109 -0
- mcp_use/client/connectors/utils.py +13 -0
- mcp_use/client/connectors/websocket.py +257 -0
- mcp_use/client/exceptions.py +31 -0
- mcp_use/client/middleware/__init__.py +50 -0
- mcp_use/client/middleware/logging.py +31 -0
- mcp_use/client/middleware/metrics.py +314 -0
- mcp_use/client/middleware/middleware.py +266 -0
- mcp_use/client/session.py +162 -0
- mcp_use/client/task_managers/__init__.py +20 -0
- mcp_use/client/task_managers/base.py +145 -0
- mcp_use/client/task_managers/sse.py +84 -0
- mcp_use/client/task_managers/stdio.py +69 -0
- mcp_use/client/task_managers/streamable_http.py +86 -0
- mcp_use/client/task_managers/websocket.py +68 -0
- mcp_use/client.py +12 -320
- mcp_use/config.py +20 -92
- mcp_use/connectors/.deprecated +0 -0
- mcp_use/connectors/__init__.py +46 -20
- mcp_use/connectors/base.py +12 -447
- mcp_use/connectors/http.py +13 -288
- mcp_use/connectors/sandbox.py +13 -297
- mcp_use/connectors/stdio.py +13 -96
- mcp_use/connectors/utils.py +15 -8
- mcp_use/connectors/websocket.py +13 -252
- mcp_use/exceptions.py +33 -18
- mcp_use/managers/.deprecated +0 -0
- mcp_use/managers/__init__.py +56 -17
- mcp_use/managers/base.py +13 -31
- mcp_use/managers/server_manager.py +13 -119
- mcp_use/managers/tools/__init__.py +45 -15
- mcp_use/managers/tools/base_tool.py +5 -16
- mcp_use/managers/tools/connect_server.py +5 -67
- mcp_use/managers/tools/disconnect_server.py +5 -41
- mcp_use/managers/tools/get_active_server.py +5 -26
- mcp_use/managers/tools/list_servers_tool.py +5 -51
- mcp_use/managers/tools/search_tools.py +17 -321
- mcp_use/middleware/.deprecated +0 -0
- mcp_use/middleware/__init__.py +89 -0
- mcp_use/middleware/logging.py +19 -0
- mcp_use/middleware/metrics.py +41 -0
- mcp_use/middleware/middleware.py +55 -0
- mcp_use/session.py +13 -149
- mcp_use/task_managers/.deprecated +0 -0
- mcp_use/task_managers/__init__.py +48 -20
- mcp_use/task_managers/base.py +13 -140
- mcp_use/task_managers/sse.py +13 -79
- mcp_use/task_managers/stdio.py +13 -64
- mcp_use/task_managers/streamable_http.py +15 -81
- mcp_use/task_managers/websocket.py +13 -63
- mcp_use/telemetry/events.py +58 -0
- mcp_use/telemetry/telemetry.py +71 -1
- mcp_use/types/.deprecated +0 -0
- mcp_use/types/sandbox.py +13 -18
- {mcp_use-1.3.11.dist-info → mcp_use-1.3.13.dist-info}/METADATA +66 -40
- mcp_use-1.3.13.dist-info/RECORD +109 -0
- mcp_use-1.3.11.dist-info/RECORD +0 -60
- mcp_use-1.3.11.dist-info/licenses/LICENSE +0 -21
- /mcp_use/{observability → agents/observability}/__init__.py +0 -0
- /mcp_use/{observability → agents/observability}/callbacks_manager.py +0 -0
- /mcp_use/{observability → agents/observability}/laminar.py +0 -0
- /mcp_use/{observability → agents/observability}/langfuse.py +0 -0
- {mcp_use-1.3.11.dist-info → mcp_use-1.3.13.dist-info}/WHEEL +0 -0
- {mcp_use-1.3.11.dist-info → mcp_use-1.3.13.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Base adapter interface for MCP tools.
|
|
3
|
+
|
|
4
|
+
This module provides the abstract base class that all MCP tool adapters should inherit from.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from abc import ABC, abstractmethod
|
|
8
|
+
from typing import TypeVar
|
|
9
|
+
|
|
10
|
+
from mcp.types import Prompt, Resource, Tool
|
|
11
|
+
|
|
12
|
+
from mcp_use.client.client import MCPClient
|
|
13
|
+
from mcp_use.client.connectors.base import BaseConnector
|
|
14
|
+
from mcp_use.logging import logger
|
|
15
|
+
from mcp_use.telemetry.telemetry import telemetry
|
|
16
|
+
|
|
17
|
+
# Generic type for the tools created by the adapter
|
|
18
|
+
T = TypeVar("T")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class BaseAdapter(ABC):
|
|
22
|
+
"""Abstract base class for converting MCP tools to other framework formats.
|
|
23
|
+
|
|
24
|
+
This class defines the common interface that all adapter implementations
|
|
25
|
+
should follow to ensure consistency across different frameworks.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __init__(self, disallowed_tools: list[str] | None = None) -> None:
|
|
29
|
+
"""Initialize a new adapter.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
disallowed_tools: list of tool names that should not be available.
|
|
33
|
+
"""
|
|
34
|
+
self.disallowed_tools = disallowed_tools or []
|
|
35
|
+
self._connector_tool_map: dict[BaseConnector, list[T]] = {}
|
|
36
|
+
|
|
37
|
+
@telemetry("adapter_create_tools")
|
|
38
|
+
async def create_tools(self, client: "MCPClient") -> list[T]:
|
|
39
|
+
"""Create tools from an MCPClient instance.
|
|
40
|
+
|
|
41
|
+
This is the recommended way to create tools from an MCPClient, as it handles
|
|
42
|
+
session creation and connector extraction automatically.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
client: The MCPClient to extract tools from.
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
A list of tools in the target framework's format.
|
|
49
|
+
|
|
50
|
+
Example:
|
|
51
|
+
```python
|
|
52
|
+
from mcp_use.client import MCPClient
|
|
53
|
+
from mcp_use.adapters import YourAdapter
|
|
54
|
+
|
|
55
|
+
client = MCPClient.from_config_file("config.json")
|
|
56
|
+
tools = await YourAdapter.create_tools(client)
|
|
57
|
+
```
|
|
58
|
+
"""
|
|
59
|
+
# Ensure we have active sessions
|
|
60
|
+
if not client.active_sessions:
|
|
61
|
+
logger.info("No active sessions found, creating new ones...")
|
|
62
|
+
await client.create_all_sessions()
|
|
63
|
+
|
|
64
|
+
# Get all active sessions
|
|
65
|
+
sessions = client.get_all_active_sessions()
|
|
66
|
+
|
|
67
|
+
# Extract connectors from sessions
|
|
68
|
+
connectors = [session.connector for session in sessions.values()]
|
|
69
|
+
|
|
70
|
+
# Create tools from connectors
|
|
71
|
+
return await self._create_tools_from_connectors(connectors)
|
|
72
|
+
|
|
73
|
+
@telemetry("adapter_load_tools")
|
|
74
|
+
async def load_tools_for_connector(self, connector: BaseConnector) -> list[T]:
|
|
75
|
+
"""Dynamically load tools for a specific connector.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
connector: The connector to load tools for.
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
The list of tools that were loaded in the target framework's format.
|
|
82
|
+
"""
|
|
83
|
+
# Check if we already have tools for this connector
|
|
84
|
+
if connector in self._connector_tool_map:
|
|
85
|
+
logger.debug(f"Returning {len(self._connector_tool_map[connector])} existing tools for connector")
|
|
86
|
+
return self._connector_tool_map[connector]
|
|
87
|
+
|
|
88
|
+
# Create tools for this connector
|
|
89
|
+
|
|
90
|
+
# Make sure the connector is initialized and has tools
|
|
91
|
+
success = await self._ensure_connector_initialized(connector)
|
|
92
|
+
if not success:
|
|
93
|
+
return []
|
|
94
|
+
|
|
95
|
+
connector_tools = []
|
|
96
|
+
# Now create tools for each MCP tool
|
|
97
|
+
for tool in await connector.list_tools():
|
|
98
|
+
# Convert the tool and add it to the list if conversion was successful
|
|
99
|
+
converted_tool = self._convert_tool(tool, connector)
|
|
100
|
+
if converted_tool:
|
|
101
|
+
connector_tools.append(converted_tool)
|
|
102
|
+
|
|
103
|
+
# Convert resources to tools so that agents can access resource content directly
|
|
104
|
+
resources_list = await connector.list_resources() or []
|
|
105
|
+
if resources_list:
|
|
106
|
+
for resource in resources_list:
|
|
107
|
+
converted_resource = self._convert_resource(resource, connector)
|
|
108
|
+
if converted_resource:
|
|
109
|
+
connector_tools.append(converted_resource)
|
|
110
|
+
|
|
111
|
+
# Convert prompts to tools so that agents can retrieve prompt content
|
|
112
|
+
prompts_list = await connector.list_prompts() or []
|
|
113
|
+
if prompts_list:
|
|
114
|
+
for prompt in prompts_list:
|
|
115
|
+
converted_prompt = self._convert_prompt(prompt, connector)
|
|
116
|
+
if converted_prompt:
|
|
117
|
+
connector_tools.append(converted_prompt)
|
|
118
|
+
# ------------------------------
|
|
119
|
+
|
|
120
|
+
# Store the tools for this connector
|
|
121
|
+
self._connector_tool_map[connector] = connector_tools
|
|
122
|
+
|
|
123
|
+
# Log available tools for debugging
|
|
124
|
+
logger.debug(
|
|
125
|
+
f"Loaded {len(connector_tools)} new tools for connector: "
|
|
126
|
+
f"{[getattr(tool, 'name', str(tool)) for tool in connector_tools]}"
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
return connector_tools
|
|
130
|
+
|
|
131
|
+
@abstractmethod
|
|
132
|
+
def _convert_tool(self, mcp_tool: Tool, connector: BaseConnector) -> T:
|
|
133
|
+
"""Convert an MCP tool to the target framework's tool format."""
|
|
134
|
+
pass
|
|
135
|
+
|
|
136
|
+
@abstractmethod
|
|
137
|
+
def _convert_resource(self, mcp_resource: Resource, connector: BaseConnector) -> T:
|
|
138
|
+
"""Convert an MCP resource to the target framework's resource format."""
|
|
139
|
+
pass
|
|
140
|
+
|
|
141
|
+
@abstractmethod
|
|
142
|
+
def _convert_prompt(self, mcp_prompt: Prompt, connector: BaseConnector) -> T:
|
|
143
|
+
"""Convert an MCP prompt to the target framework's prompt format."""
|
|
144
|
+
pass
|
|
145
|
+
|
|
146
|
+
async def _create_tools_from_connectors(self, connectors: list[BaseConnector]) -> list[T]:
|
|
147
|
+
"""Create tools from MCP tools in all provided connectors.
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
connectors: list of MCP connectors to create tools from.
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
A list of tools in the target framework's format.
|
|
154
|
+
"""
|
|
155
|
+
tools = []
|
|
156
|
+
for connector in connectors:
|
|
157
|
+
# Create tools for this connector
|
|
158
|
+
connector_tools = await self.load_tools_for_connector(connector)
|
|
159
|
+
tools.extend(connector_tools)
|
|
160
|
+
|
|
161
|
+
# Log available tools for debugging
|
|
162
|
+
logger.debug(f"Available tools: {len(tools)}")
|
|
163
|
+
return tools
|
|
164
|
+
|
|
165
|
+
def _check_connector_initialized(self, connector: BaseConnector) -> bool:
|
|
166
|
+
"""Check if a connector is initialized and has tools.
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
connector: The connector to check.
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
True if the connector is initialized and has tools, False otherwise.
|
|
173
|
+
"""
|
|
174
|
+
return hasattr(connector, "tools") and connector.tools
|
|
175
|
+
|
|
176
|
+
async def _ensure_connector_initialized(self, connector: BaseConnector) -> bool:
|
|
177
|
+
"""Ensure a connector is initialized.
|
|
178
|
+
|
|
179
|
+
Args:
|
|
180
|
+
connector: The connector to initialize.
|
|
181
|
+
|
|
182
|
+
Returns:
|
|
183
|
+
True if initialization succeeded, False otherwise.
|
|
184
|
+
"""
|
|
185
|
+
if not self._check_connector_initialized(connector):
|
|
186
|
+
logger.debug("Connector doesn't have tools, initializing it")
|
|
187
|
+
try:
|
|
188
|
+
await connector.initialize()
|
|
189
|
+
return True
|
|
190
|
+
except Exception as e:
|
|
191
|
+
logger.error(f"Error initializing connector: {e}")
|
|
192
|
+
return False
|
|
193
|
+
return True
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
"""
|
|
2
|
+
LangChain adapter for MCP tools.
|
|
3
|
+
|
|
4
|
+
This module provides utilities to convert MCP tools to LangChain tools.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import re
|
|
8
|
+
from typing import Any, NoReturn
|
|
9
|
+
|
|
10
|
+
from jsonschema_pydantic import jsonschema_to_pydantic
|
|
11
|
+
from langchain_core.tools import BaseTool
|
|
12
|
+
from mcp.types import (
|
|
13
|
+
CallToolResult,
|
|
14
|
+
Prompt,
|
|
15
|
+
ReadResourceRequestParams,
|
|
16
|
+
Resource,
|
|
17
|
+
)
|
|
18
|
+
from pydantic import BaseModel, Field, create_model
|
|
19
|
+
|
|
20
|
+
from mcp_use.agents.adapters.base import BaseAdapter
|
|
21
|
+
from mcp_use.client.connectors.base import BaseConnector
|
|
22
|
+
from mcp_use.errors.error_formatting import format_error
|
|
23
|
+
from mcp_use.logging import logger
|
|
24
|
+
from mcp_use.telemetry.telemetry import telemetry
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class LangChainAdapter(BaseAdapter):
|
|
28
|
+
"""Adapter for converting MCP tools to LangChain tools."""
|
|
29
|
+
|
|
30
|
+
def __init__(self, disallowed_tools: list[str] | None = None) -> None:
|
|
31
|
+
"""Initialize a new LangChain adapter.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
disallowed_tools: list of tool names that should not be available.
|
|
35
|
+
"""
|
|
36
|
+
super().__init__(disallowed_tools)
|
|
37
|
+
self._connector_tool_map: dict[BaseConnector, list[BaseTool]] = {}
|
|
38
|
+
|
|
39
|
+
@telemetry("adapter_fix_schema")
|
|
40
|
+
def fix_schema(self, schema: dict) -> dict:
|
|
41
|
+
"""Convert JSON Schema 'type': ['string', 'null'] to 'anyOf' format and fix enum handling.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
schema: The JSON schema to fix.
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
The fixed JSON schema.
|
|
48
|
+
"""
|
|
49
|
+
if isinstance(schema, dict):
|
|
50
|
+
if "type" in schema and isinstance(schema["type"], list):
|
|
51
|
+
schema["anyOf"] = [{"type": t} for t in schema["type"]]
|
|
52
|
+
del schema["type"] # Remove 'type' and standardize to 'anyOf'
|
|
53
|
+
|
|
54
|
+
# Fix enum handling - ensure enum fields are properly typed as strings
|
|
55
|
+
if "enum" in schema and "type" not in schema:
|
|
56
|
+
schema["type"] = "string"
|
|
57
|
+
|
|
58
|
+
for key, value in schema.items():
|
|
59
|
+
schema[key] = self.fix_schema(value) # Apply recursively
|
|
60
|
+
return schema
|
|
61
|
+
|
|
62
|
+
@telemetry("adapter_convert_tool")
|
|
63
|
+
def _convert_tool(self, mcp_tool: dict[str, Any], connector: BaseConnector) -> BaseTool:
|
|
64
|
+
"""Convert an MCP tool to LangChain's tool format.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
mcp_tool: The MCP tool to convert.
|
|
68
|
+
connector: The connector that provides this tool.
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
A LangChain BaseTool.
|
|
72
|
+
"""
|
|
73
|
+
# Skip disallowed tools
|
|
74
|
+
if mcp_tool.name in self.disallowed_tools:
|
|
75
|
+
return None
|
|
76
|
+
|
|
77
|
+
# This is a dynamic class creation, we need to work with the self reference
|
|
78
|
+
adapter_self = self
|
|
79
|
+
|
|
80
|
+
class McpToLangChainAdapter(BaseTool):
|
|
81
|
+
name: str = mcp_tool.name or "NO NAME"
|
|
82
|
+
description: str = mcp_tool.description or ""
|
|
83
|
+
# Convert JSON schema to Pydantic model for argument validation
|
|
84
|
+
args_schema: type[BaseModel] = jsonschema_to_pydantic(
|
|
85
|
+
adapter_self.fix_schema(mcp_tool.inputSchema) # Apply schema conversion
|
|
86
|
+
)
|
|
87
|
+
tool_connector: BaseConnector = connector # Renamed variable to avoid name conflict
|
|
88
|
+
handle_tool_error: bool = True
|
|
89
|
+
|
|
90
|
+
def __repr__(self) -> str:
|
|
91
|
+
return f"MCP tool: {self.name}: {self.description}"
|
|
92
|
+
|
|
93
|
+
def _run(self, **kwargs: Any) -> NoReturn:
|
|
94
|
+
"""Synchronous run method that always raises an error.
|
|
95
|
+
|
|
96
|
+
Raises:
|
|
97
|
+
NotImplementedError: Always raises this error because MCP tools
|
|
98
|
+
only support async operations.
|
|
99
|
+
"""
|
|
100
|
+
raise NotImplementedError("MCP tools only support async operations")
|
|
101
|
+
|
|
102
|
+
async def _arun(self, **kwargs: Any) -> str | dict:
|
|
103
|
+
"""Asynchronously execute the tool with given arguments.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
kwargs: The arguments to pass to the tool.
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
The result of the tool execution.
|
|
110
|
+
|
|
111
|
+
Raises:
|
|
112
|
+
ToolException: If tool execution fails.
|
|
113
|
+
"""
|
|
114
|
+
logger.debug(f'MCP tool: "{self.name}" received input: {kwargs}')
|
|
115
|
+
|
|
116
|
+
try:
|
|
117
|
+
tool_result: CallToolResult = await self.tool_connector.call_tool(self.name, kwargs)
|
|
118
|
+
try:
|
|
119
|
+
return str(tool_result.content)
|
|
120
|
+
except Exception as e:
|
|
121
|
+
# Log the exception for debugging
|
|
122
|
+
logger.error(f"Error parsing tool result: {e}")
|
|
123
|
+
return format_error(e, tool=self.name, tool_content=tool_result.content)
|
|
124
|
+
|
|
125
|
+
except Exception as e:
|
|
126
|
+
if self.handle_tool_error:
|
|
127
|
+
return format_error(e, tool=self.name) # Format the error to make LLM understand it
|
|
128
|
+
raise
|
|
129
|
+
|
|
130
|
+
return McpToLangChainAdapter()
|
|
131
|
+
|
|
132
|
+
def _convert_resource(self, mcp_resource: Resource, connector: BaseConnector) -> BaseTool:
|
|
133
|
+
"""Convert an MCP resource to LangChain's tool format.
|
|
134
|
+
|
|
135
|
+
Each resource becomes an async tool that returns its content when called.
|
|
136
|
+
The tool takes **no** arguments because the resource URI is fixed.
|
|
137
|
+
"""
|
|
138
|
+
|
|
139
|
+
def _sanitize(name: str) -> str:
|
|
140
|
+
return re.sub(r"[^A-Za-z0-9_]+", "_", name).lower().strip("_")
|
|
141
|
+
|
|
142
|
+
class ResourceTool(BaseTool):
|
|
143
|
+
name: str = _sanitize(mcp_resource.name or f"resource_{mcp_resource.uri}")
|
|
144
|
+
description: str = (
|
|
145
|
+
mcp_resource.description or f"Return the content of the resource located at URI {mcp_resource.uri}."
|
|
146
|
+
)
|
|
147
|
+
args_schema: type[BaseModel] = ReadResourceRequestParams
|
|
148
|
+
tool_connector: BaseConnector = connector
|
|
149
|
+
handle_tool_error: bool = True
|
|
150
|
+
|
|
151
|
+
def _run(self, **kwargs: Any) -> NoReturn:
|
|
152
|
+
raise NotImplementedError("Resource tools only support async operations")
|
|
153
|
+
|
|
154
|
+
async def _arun(self, **kwargs: Any) -> Any:
|
|
155
|
+
logger.debug(f'Resource tool: "{self.name}" called')
|
|
156
|
+
try:
|
|
157
|
+
result = await self.tool_connector.read_resource(mcp_resource.uri)
|
|
158
|
+
for content in result.contents:
|
|
159
|
+
# Attempt to decode bytes if necessary
|
|
160
|
+
if isinstance(content, bytes):
|
|
161
|
+
content_decoded = content.decode()
|
|
162
|
+
else:
|
|
163
|
+
content_decoded = str(content)
|
|
164
|
+
|
|
165
|
+
return content_decoded
|
|
166
|
+
except Exception as e:
|
|
167
|
+
if self.handle_tool_error:
|
|
168
|
+
return format_error(e, tool=self.name) # Format the error to make LLM understand it
|
|
169
|
+
raise
|
|
170
|
+
|
|
171
|
+
return ResourceTool()
|
|
172
|
+
|
|
173
|
+
def _convert_prompt(self, mcp_prompt: Prompt, connector: BaseConnector) -> BaseTool:
|
|
174
|
+
"""Convert an MCP prompt to LangChain's tool format.
|
|
175
|
+
|
|
176
|
+
The resulting tool executes `get_prompt` on the connector with the prompt's name and
|
|
177
|
+
the user-provided arguments (if any). The tool returns the decoded prompt content.
|
|
178
|
+
"""
|
|
179
|
+
prompt_arguments = mcp_prompt.arguments
|
|
180
|
+
|
|
181
|
+
# Sanitize the prompt name to create a valid Python identifier for the model name
|
|
182
|
+
base_model_name = re.sub(r"[^a-zA-Z0-9_]", "_", mcp_prompt.name)
|
|
183
|
+
if not base_model_name or base_model_name[0].isdigit():
|
|
184
|
+
base_model_name = "PromptArgs_" + base_model_name
|
|
185
|
+
dynamic_model_name = f"{base_model_name}_InputSchema"
|
|
186
|
+
|
|
187
|
+
if prompt_arguments:
|
|
188
|
+
field_definitions_for_create: dict[str, Any] = {}
|
|
189
|
+
for arg in prompt_arguments:
|
|
190
|
+
param_type: type = getattr(arg, "type", str)
|
|
191
|
+
if arg.required:
|
|
192
|
+
field_definitions_for_create[arg.name] = (
|
|
193
|
+
param_type,
|
|
194
|
+
Field(description=arg.description),
|
|
195
|
+
)
|
|
196
|
+
else:
|
|
197
|
+
field_definitions_for_create[arg.name] = (
|
|
198
|
+
param_type | None,
|
|
199
|
+
Field(None, description=arg.description),
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
InputSchema = create_model(dynamic_model_name, **field_definitions_for_create, __base__=BaseModel)
|
|
203
|
+
else:
|
|
204
|
+
# Create an empty Pydantic model if there are no arguments
|
|
205
|
+
InputSchema = create_model(dynamic_model_name, __base__=BaseModel)
|
|
206
|
+
|
|
207
|
+
class PromptTool(BaseTool):
|
|
208
|
+
name: str = mcp_prompt.name
|
|
209
|
+
description: str = mcp_prompt.description
|
|
210
|
+
|
|
211
|
+
args_schema: type[BaseModel] = InputSchema
|
|
212
|
+
tool_connector: BaseConnector = connector
|
|
213
|
+
handle_tool_error: bool = True
|
|
214
|
+
|
|
215
|
+
def _run(self, **kwargs: Any) -> NoReturn:
|
|
216
|
+
raise NotImplementedError("Prompt tools only support async operations")
|
|
217
|
+
|
|
218
|
+
async def _arun(self, **kwargs: Any) -> Any:
|
|
219
|
+
logger.debug(f'Prompt tool: "{self.name}" called with args: {kwargs}')
|
|
220
|
+
try:
|
|
221
|
+
result = await self.tool_connector.get_prompt(self.name, kwargs)
|
|
222
|
+
return result.messages
|
|
223
|
+
except Exception as e:
|
|
224
|
+
if self.handle_tool_error:
|
|
225
|
+
return format_error(e, tool=self.name) # Format the error to make LLM understand it
|
|
226
|
+
raise
|
|
227
|
+
|
|
228
|
+
return PromptTool()
|
mcp_use/agents/base.py
CHANGED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from .server_manager import ServerManager
|
|
2
|
+
from .tools import (
|
|
3
|
+
ConnectServerTool,
|
|
4
|
+
DisconnectServerTool,
|
|
5
|
+
GetActiveServerTool,
|
|
6
|
+
ListServersTool,
|
|
7
|
+
MCPServerTool,
|
|
8
|
+
SearchToolsTool,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"ServerManager",
|
|
13
|
+
"MCPServerTool",
|
|
14
|
+
"ConnectServerTool",
|
|
15
|
+
"DisconnectServerTool",
|
|
16
|
+
"GetActiveServerTool",
|
|
17
|
+
"ListServersTool",
|
|
18
|
+
"SearchToolsTool",
|
|
19
|
+
]
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
|
|
3
|
+
from langchain_core.tools import BaseTool
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class BaseServerManager(ABC):
|
|
7
|
+
"""Abstract base class for server managers.
|
|
8
|
+
|
|
9
|
+
This class defines the interface for server managers that can be used with MCPAgent.
|
|
10
|
+
Custom server managers should inherit from this class and implement the required methods.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
@abstractmethod
|
|
14
|
+
async def initialize(self) -> None:
|
|
15
|
+
"""Initialize the server manager."""
|
|
16
|
+
raise NotImplementedError
|
|
17
|
+
|
|
18
|
+
@property
|
|
19
|
+
@abstractmethod
|
|
20
|
+
def tools(self) -> list[BaseTool]:
|
|
21
|
+
"""Get all server management tools and tools from the active server.
|
|
22
|
+
|
|
23
|
+
Returns:
|
|
24
|
+
list of LangChain tools for server management plus tools from active server
|
|
25
|
+
"""
|
|
26
|
+
raise NotImplementedError
|
|
27
|
+
|
|
28
|
+
@abstractmethod
|
|
29
|
+
def has_tool_changes(self, current_tool_names: set[str]) -> bool:
|
|
30
|
+
"""Check if the available tools have changed.
|
|
31
|
+
Args:
|
|
32
|
+
current_tool_names: Set of currently known tool names
|
|
33
|
+
Returns:
|
|
34
|
+
True if tools have changed, False otherwise
|
|
35
|
+
"""
|
|
36
|
+
raise NotImplementedError
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
from langchain_core.tools import BaseTool
|
|
2
|
+
|
|
3
|
+
from mcp_use.agents.adapters.base import BaseAdapter
|
|
4
|
+
from mcp_use.agents.managers.base import BaseServerManager
|
|
5
|
+
from mcp_use.agents.managers.tools import (
|
|
6
|
+
ConnectServerTool,
|
|
7
|
+
DisconnectServerTool,
|
|
8
|
+
GetActiveServerTool,
|
|
9
|
+
ListServersTool,
|
|
10
|
+
SearchToolsTool,
|
|
11
|
+
)
|
|
12
|
+
from mcp_use.client import MCPClient
|
|
13
|
+
from mcp_use.logging import logger
|
|
14
|
+
from mcp_use.telemetry.telemetry import telemetry
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ServerManager(BaseServerManager):
|
|
18
|
+
"""Manages MCP servers and provides tools for server selection and management.
|
|
19
|
+
|
|
20
|
+
This class allows an agent to discover and select which MCP server to use,
|
|
21
|
+
dynamically activating the tools for the selected server.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def __init__(self, client: MCPClient, adapter: BaseAdapter) -> None:
|
|
25
|
+
"""Initialize the server manager.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
client: The MCPClient instance managing server connections
|
|
29
|
+
adapter: The LangChainAdapter for converting MCP tools to LangChain tools
|
|
30
|
+
"""
|
|
31
|
+
self.client = client
|
|
32
|
+
self.adapter = adapter
|
|
33
|
+
self.active_server: str | None = None
|
|
34
|
+
self.initialized_servers: dict[str, bool] = {}
|
|
35
|
+
self._server_tools: dict[str, list[BaseTool]] = {}
|
|
36
|
+
|
|
37
|
+
@telemetry("server_manager_initialize")
|
|
38
|
+
async def initialize(self) -> None:
|
|
39
|
+
"""Initialize the server manager and prepare server management tools."""
|
|
40
|
+
# Make sure we have server configurations
|
|
41
|
+
if not self.client.get_server_names():
|
|
42
|
+
logger.warning("No MCP servers defined in client configuration")
|
|
43
|
+
|
|
44
|
+
async def _prefetch_server_tools(self) -> None:
|
|
45
|
+
"""Pre-fetch tools for all servers to populate the tool search index."""
|
|
46
|
+
servers = self.client.get_server_names()
|
|
47
|
+
for server_name in servers:
|
|
48
|
+
try:
|
|
49
|
+
# Only create session if needed, don't set active
|
|
50
|
+
session = None
|
|
51
|
+
try:
|
|
52
|
+
session = self.client.get_session(server_name)
|
|
53
|
+
logger.debug(f"Using existing session for server '{server_name}' to prefetch tools.")
|
|
54
|
+
except ValueError:
|
|
55
|
+
try:
|
|
56
|
+
session = await self.client.create_session(server_name)
|
|
57
|
+
logger.debug(f"Temporarily created session for '{server_name}' to prefetch tools")
|
|
58
|
+
except Exception:
|
|
59
|
+
logger.warning(f"Could not create session for '{server_name}' during prefetch")
|
|
60
|
+
continue
|
|
61
|
+
|
|
62
|
+
# Fetch tools if session is available
|
|
63
|
+
if session:
|
|
64
|
+
connector = session.connector
|
|
65
|
+
tools = await self.adapter._create_tools_from_connectors([connector])
|
|
66
|
+
|
|
67
|
+
# Check if this server's tools have changed
|
|
68
|
+
if server_name not in self._server_tools or self._server_tools[server_name] != tools:
|
|
69
|
+
self._server_tools[server_name] = tools # Cache tools
|
|
70
|
+
self.initialized_servers[server_name] = True # Mark as initialized
|
|
71
|
+
logger.debug(f"Prefetched {len(tools)} tools for server '{server_name}'.")
|
|
72
|
+
else:
|
|
73
|
+
logger.debug(f"Tools for server '{server_name}' unchanged, using cached version.")
|
|
74
|
+
except Exception as e:
|
|
75
|
+
logger.error(f"Error prefetching tools for server '{server_name}': {e}")
|
|
76
|
+
|
|
77
|
+
def get_active_server_tools(self) -> list[BaseTool]:
|
|
78
|
+
"""Get tools from the currently active server.
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
List of tools from the active server, or empty list if no server is active
|
|
82
|
+
"""
|
|
83
|
+
if self.active_server and self.active_server in self._server_tools:
|
|
84
|
+
return self._server_tools[self.active_server]
|
|
85
|
+
return []
|
|
86
|
+
|
|
87
|
+
def get_management_tools(self) -> list[BaseTool]:
|
|
88
|
+
"""Get the server management tools.
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
List of server management tools
|
|
92
|
+
"""
|
|
93
|
+
return [
|
|
94
|
+
ListServersTool(self),
|
|
95
|
+
ConnectServerTool(self),
|
|
96
|
+
GetActiveServerTool(self),
|
|
97
|
+
DisconnectServerTool(self),
|
|
98
|
+
SearchToolsTool(self),
|
|
99
|
+
]
|
|
100
|
+
|
|
101
|
+
def has_tool_changes(self, current_tool_names: set[str]) -> bool:
|
|
102
|
+
"""Check if the available tools have changed.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
current_tool_names: Set of currently known tool names
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
True if tools have changed, False otherwise
|
|
109
|
+
"""
|
|
110
|
+
new_tool_names = {tool.name for tool in self.tools}
|
|
111
|
+
return new_tool_names != current_tool_names
|
|
112
|
+
|
|
113
|
+
@property
|
|
114
|
+
def tools(self) -> list[BaseTool]:
|
|
115
|
+
"""Get all server management tools and tools from the active server.
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
list of LangChain tools for server management plus tools from active server
|
|
119
|
+
"""
|
|
120
|
+
management_tools = self.get_management_tools()
|
|
121
|
+
|
|
122
|
+
# Add tools from the active server if available
|
|
123
|
+
if self.active_server and self.active_server in self._server_tools:
|
|
124
|
+
server_tools = self._server_tools[self.active_server]
|
|
125
|
+
logger.debug(f"Including {len(server_tools)} tools from active server '{self.active_server}'")
|
|
126
|
+
logger.debug(f"Server tools: {[tool.name for tool in server_tools]}")
|
|
127
|
+
return management_tools + server_tools
|
|
128
|
+
else:
|
|
129
|
+
logger.debug("No active server - returning only management tools")
|
|
130
|
+
|
|
131
|
+
return management_tools
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from .base_tool import MCPServerTool
|
|
2
|
+
from .connect_server import ConnectServerTool
|
|
3
|
+
from .disconnect_server import DisconnectServerTool
|
|
4
|
+
from .get_active_server import GetActiveServerTool
|
|
5
|
+
from .list_servers_tool import ListServersTool
|
|
6
|
+
from .search_tools import SearchToolsTool
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"MCPServerTool",
|
|
10
|
+
"ConnectServerTool",
|
|
11
|
+
"DisconnectServerTool",
|
|
12
|
+
"GetActiveServerTool",
|
|
13
|
+
"ListServersTool",
|
|
14
|
+
"SearchToolsTool",
|
|
15
|
+
]
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from typing import ClassVar
|
|
2
|
+
|
|
3
|
+
from langchain_core.tools import BaseTool
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class MCPServerTool(BaseTool):
|
|
7
|
+
"""Base tool for MCP server operations."""
|
|
8
|
+
|
|
9
|
+
name: ClassVar[str] = "mcp_server_tool"
|
|
10
|
+
description: ClassVar[str] = "Base tool for MCP server operations."
|
|
11
|
+
|
|
12
|
+
def __init__(self, server_manager):
|
|
13
|
+
"""Initialize with server manager."""
|
|
14
|
+
super().__init__()
|
|
15
|
+
self._server_manager = server_manager
|
|
16
|
+
|
|
17
|
+
@property
|
|
18
|
+
def server_manager(self):
|
|
19
|
+
return self._server_manager
|