mcp-use 1.3.12__py3-none-any.whl → 1.4.0__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 -219
- mcp_use/agents/adapters/__init__.py +17 -0
- mcp_use/agents/adapters/anthropic.py +93 -0
- mcp_use/agents/adapters/base.py +316 -0
- mcp_use/agents/adapters/google.py +103 -0
- mcp_use/agents/adapters/langchain_adapter.py +212 -0
- mcp_use/agents/adapters/openai.py +111 -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 +386 -485
- mcp_use/agents/prompts/system_prompt_builder.py +1 -1
- mcp_use/agents/remote.py +15 -2
- 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 +215 -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 -344
- mcp_use/config.py +20 -97
- mcp_use/connectors/.deprecated +0 -0
- mcp_use/connectors/__init__.py +46 -20
- mcp_use/connectors/base.py +12 -455
- mcp_use/connectors/http.py +13 -300
- mcp_use/connectors/sandbox.py +13 -306
- mcp_use/connectors/stdio.py +13 -104
- mcp_use/connectors/utils.py +15 -8
- mcp_use/connectors/websocket.py +13 -252
- mcp_use/exceptions.py +33 -18
- mcp_use/logging.py +1 -1
- 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 -50
- mcp_use/middleware/logging.py +14 -26
- mcp_use/middleware/metrics.py +30 -303
- mcp_use/middleware/middleware.py +39 -246
- 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/telemetry/utils.py +1 -1
- mcp_use/types/.deprecated +0 -0
- mcp_use/types/sandbox.py +13 -18
- {mcp_use-1.3.12.dist-info → mcp_use-1.4.0.dist-info}/METADATA +68 -43
- mcp_use-1.4.0.dist-info/RECORD +111 -0
- mcp_use/cli.py +0 -581
- mcp_use-1.3.12.dist-info/RECORD +0 -64
- mcp_use-1.3.12.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.12.dist-info → mcp_use-1.4.0.dist-info}/WHEEL +0 -0
- {mcp_use-1.3.12.dist-info → mcp_use-1.4.0.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,316 @@
|
|
|
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 Any, 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
|
+
|
|
36
|
+
# Three maps to optimize and cache only what is needed
|
|
37
|
+
self._connector_tool_map: dict[BaseConnector, list[T]] = {}
|
|
38
|
+
self._connector_resource_map: dict[BaseConnector, list[T]] = {}
|
|
39
|
+
self._connector_prompt_map: dict[BaseConnector, list[T]] = {}
|
|
40
|
+
|
|
41
|
+
self.tools: list[T] = []
|
|
42
|
+
self.resources: list[T] = []
|
|
43
|
+
self.prompts: list[T] = []
|
|
44
|
+
|
|
45
|
+
def parse_result(self, tool_result: Any) -> str:
|
|
46
|
+
"""Parse the result from any MCP operation (tool, resource, or prompt) into a string.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
tool_result: The result object from an MCP operation.
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
A string representation of the result content.
|
|
53
|
+
"""
|
|
54
|
+
if getattr(tool_result, "isError", False):
|
|
55
|
+
# Handle errors first
|
|
56
|
+
error_content = tool_result.content or "Unknown error"
|
|
57
|
+
return f"Error: {error_content}"
|
|
58
|
+
elif hasattr(tool_result, "contents"): # For Resources (ReadResourceResult)
|
|
59
|
+
return "\n".join(c.decode() if isinstance(c, bytes) else str(c) for c in tool_result.contents)
|
|
60
|
+
elif hasattr(tool_result, "messages"): # For Prompts (GetPromptResult)
|
|
61
|
+
return "\n".join(str(s) for s in tool_result.messages)
|
|
62
|
+
elif hasattr(tool_result, "content"): # For Tools (CallToolResult)
|
|
63
|
+
return str(tool_result.content)
|
|
64
|
+
else:
|
|
65
|
+
# Fallback for unexpected types
|
|
66
|
+
return str(tool_result)
|
|
67
|
+
|
|
68
|
+
@telemetry("adapter_fix_schema")
|
|
69
|
+
def fix_schema(self, schema: dict) -> dict:
|
|
70
|
+
"""Convert JSON Schema 'type': ['string', 'null'] to 'anyOf' format and fix enum handling.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
schema: The JSON schema to fix.
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
The fixed JSON schema.
|
|
77
|
+
"""
|
|
78
|
+
if isinstance(schema, dict):
|
|
79
|
+
if "type" in schema and isinstance(schema["type"], list):
|
|
80
|
+
schema["anyOf"] = [{"type": t} for t in schema["type"]]
|
|
81
|
+
del schema["type"] # Remove 'type' and standardize to 'anyOf'
|
|
82
|
+
|
|
83
|
+
# Fix enum handling - ensure enum fields are properly typed as strings
|
|
84
|
+
if "enum" in schema and "type" not in schema:
|
|
85
|
+
schema["type"] = "string"
|
|
86
|
+
|
|
87
|
+
for key, value in schema.items():
|
|
88
|
+
schema[key] = self.fix_schema(value) # Apply recursively
|
|
89
|
+
return schema
|
|
90
|
+
|
|
91
|
+
async def _get_connectors(self, client: MCPClient) -> list[BaseConnector]:
|
|
92
|
+
"""Get all connectors from the client, creating sessions if needed."""
|
|
93
|
+
if not client.active_sessions:
|
|
94
|
+
logger.info("No active sessions found, creating new ones...")
|
|
95
|
+
await client.create_all_sessions()
|
|
96
|
+
|
|
97
|
+
sessions = client.get_all_active_sessions()
|
|
98
|
+
return [session.connector for session in sessions.values()]
|
|
99
|
+
|
|
100
|
+
async def create_all(self, client: MCPClient) -> None:
|
|
101
|
+
"""Create tools, resources, and prompts from an MCPClient instance."""
|
|
102
|
+
await self.create_tools(client)
|
|
103
|
+
await self.create_resources(client)
|
|
104
|
+
await self.create_prompts(client)
|
|
105
|
+
|
|
106
|
+
async def create_tools(self, client: MCPClient) -> list[T]:
|
|
107
|
+
"""Create tools from the MCPClient instance.
|
|
108
|
+
|
|
109
|
+
This handles session creation and connector extraction automatically.
|
|
110
|
+
The created tools are stored in `self.tools`.
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
A list of tools in the target framework's format.
|
|
114
|
+
"""
|
|
115
|
+
connectors = await self._get_connectors(client)
|
|
116
|
+
self.tools = await self._create_tools_from_connectors(connectors)
|
|
117
|
+
|
|
118
|
+
return self.tools
|
|
119
|
+
|
|
120
|
+
async def create_resources(self, client: MCPClient) -> list[T]:
|
|
121
|
+
"""Create resources from the MCPClient instance.
|
|
122
|
+
|
|
123
|
+
This handles session creation and connector extraction automatically.
|
|
124
|
+
The created resources are stored in `self.resources`.
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
A list of resources in the target framework's format.
|
|
128
|
+
"""
|
|
129
|
+
connectors = await self._get_connectors(client)
|
|
130
|
+
self.resources = await self._create_resources_from_connectors(connectors)
|
|
131
|
+
|
|
132
|
+
return self.resources
|
|
133
|
+
|
|
134
|
+
async def create_prompts(self, client: MCPClient) -> list[T]:
|
|
135
|
+
"""Create prompts from the MCPClient instance.
|
|
136
|
+
|
|
137
|
+
This handles session creation and connector extraction automatically.
|
|
138
|
+
The created prompts are stored in `self.prompts`.
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
A list of prompts in the target framework's format.
|
|
142
|
+
"""
|
|
143
|
+
connectors = await self._get_connectors(client)
|
|
144
|
+
self.prompts = await self._create_prompts_from_connectors(connectors)
|
|
145
|
+
|
|
146
|
+
return self.prompts
|
|
147
|
+
|
|
148
|
+
@telemetry("adapter_load_tools")
|
|
149
|
+
async def load_tools_for_connector(self, connector: BaseConnector) -> list[T]:
|
|
150
|
+
"""Dynamically load tools for a specific connector.
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
connector: The connector to load tools for.
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
The list of tools that were loaded in the target framework's format.
|
|
157
|
+
"""
|
|
158
|
+
# Check if we already have tools for this connector
|
|
159
|
+
if connector in self._connector_tool_map:
|
|
160
|
+
logger.debug(f"Returning {len(self._connector_tool_map[connector])} existing tools for connector")
|
|
161
|
+
return self._connector_tool_map[connector]
|
|
162
|
+
|
|
163
|
+
if not await self._ensure_connector_initialized(connector):
|
|
164
|
+
return []
|
|
165
|
+
|
|
166
|
+
connector_tools = []
|
|
167
|
+
for tool in await connector.list_tools():
|
|
168
|
+
converted_tool = self._convert_tool(tool, connector)
|
|
169
|
+
if converted_tool:
|
|
170
|
+
connector_tools.append(converted_tool)
|
|
171
|
+
|
|
172
|
+
# Store the tools for this connector
|
|
173
|
+
self._connector_tool_map[connector] = connector_tools
|
|
174
|
+
|
|
175
|
+
# Log available tools for debugging
|
|
176
|
+
logger.debug(
|
|
177
|
+
f"Loaded {len(connector_tools)} new tools for connector: "
|
|
178
|
+
f"{[getattr(tool, 'name', str(tool)) for tool in connector_tools]}"
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
return connector_tools
|
|
182
|
+
|
|
183
|
+
@telemetry("adapter_load_resources")
|
|
184
|
+
async def load_resources_for_connector(self, connector: BaseConnector):
|
|
185
|
+
"""Dynamically load resources for a specific connector.
|
|
186
|
+
|
|
187
|
+
Args:
|
|
188
|
+
connector: The connector to load resources for.
|
|
189
|
+
|
|
190
|
+
Returns:
|
|
191
|
+
The list of resources that were loaded in the target framework's format.
|
|
192
|
+
"""
|
|
193
|
+
if connector in self._connector_resource_map:
|
|
194
|
+
logger.debug(f"Returning {len(self._connector_resource_map[connector])} existing resources for connector")
|
|
195
|
+
return self._connector_resource_map[connector]
|
|
196
|
+
|
|
197
|
+
if not await self._ensure_connector_initialized(connector):
|
|
198
|
+
return []
|
|
199
|
+
|
|
200
|
+
connector_resources = []
|
|
201
|
+
for resource in await connector.list_resources() or []:
|
|
202
|
+
converted_resource = self._convert_resource(resource, connector)
|
|
203
|
+
if converted_resource:
|
|
204
|
+
connector_resources.append(converted_resource)
|
|
205
|
+
|
|
206
|
+
self._connector_resource_map[connector] = connector_resources
|
|
207
|
+
logger.debug(
|
|
208
|
+
f"Loaded {len(connector_resources)} new resources for connector: "
|
|
209
|
+
f"{[getattr(r, 'name', str(r)) for r in connector_resources]}"
|
|
210
|
+
)
|
|
211
|
+
return connector_resources
|
|
212
|
+
|
|
213
|
+
@telemetry("adapter_load_prompts")
|
|
214
|
+
async def load_prompts_for_connector(self, connector: BaseConnector) -> list[T]:
|
|
215
|
+
"""Dynamically load prompts for a specific connector.
|
|
216
|
+
|
|
217
|
+
Args:
|
|
218
|
+
connector: The connector to load prompts for.
|
|
219
|
+
|
|
220
|
+
Returns:
|
|
221
|
+
The list of prompts that were loaded in the target framework's format.
|
|
222
|
+
"""
|
|
223
|
+
if connector in self._connector_prompt_map:
|
|
224
|
+
logger.debug(f"Returning {len(self._connector_prompt_map[connector])} existing prompts for connector")
|
|
225
|
+
return self._connector_prompt_map[connector]
|
|
226
|
+
|
|
227
|
+
if not await self._ensure_connector_initialized(connector):
|
|
228
|
+
return []
|
|
229
|
+
|
|
230
|
+
connector_prompts = []
|
|
231
|
+
for prompt in await connector.list_prompts() or []:
|
|
232
|
+
converted_prompt = self._convert_prompt(prompt, connector)
|
|
233
|
+
if converted_prompt:
|
|
234
|
+
connector_prompts.append(converted_prompt)
|
|
235
|
+
|
|
236
|
+
self._connector_prompt_map[connector] = connector_prompts
|
|
237
|
+
logger.debug(
|
|
238
|
+
f"Loaded {len(connector_prompts)} new prompts for connector: "
|
|
239
|
+
f"{[getattr(p, 'name', str(p)) for p in connector_prompts]}"
|
|
240
|
+
)
|
|
241
|
+
return connector_prompts
|
|
242
|
+
|
|
243
|
+
@abstractmethod
|
|
244
|
+
def _convert_tool(self, mcp_tool: Tool, connector: BaseConnector) -> T:
|
|
245
|
+
"""Convert an MCP tool to the target framework's tool format."""
|
|
246
|
+
pass
|
|
247
|
+
|
|
248
|
+
@abstractmethod
|
|
249
|
+
def _convert_resource(self, mcp_resource: Resource, connector: BaseConnector) -> T:
|
|
250
|
+
"""Convert an MCP resource to the target framework's resource format."""
|
|
251
|
+
pass
|
|
252
|
+
|
|
253
|
+
@abstractmethod
|
|
254
|
+
def _convert_prompt(self, mcp_prompt: Prompt, connector: BaseConnector) -> T:
|
|
255
|
+
"""Convert an MCP prompt to the target framework's prompt format."""
|
|
256
|
+
pass
|
|
257
|
+
|
|
258
|
+
async def _create_tools_from_connectors(self, connectors: list[BaseConnector]) -> list[T]:
|
|
259
|
+
"""Create tools from MCP tools in all provided connectors."""
|
|
260
|
+
tools = []
|
|
261
|
+
for connector in connectors:
|
|
262
|
+
connector_tools = await self.load_tools_for_connector(connector)
|
|
263
|
+
tools.extend(connector_tools)
|
|
264
|
+
|
|
265
|
+
logger.debug(f"Available tools: {len(tools)}")
|
|
266
|
+
return tools
|
|
267
|
+
|
|
268
|
+
async def _create_resources_from_connectors(self, connectors: list[BaseConnector]) -> list[T]:
|
|
269
|
+
"""Create resources from MCP resources in all provided connectors."""
|
|
270
|
+
resources = []
|
|
271
|
+
for connector in connectors:
|
|
272
|
+
connector_resources = await self.load_resources_for_connector(connector)
|
|
273
|
+
resources.extend(connector_resources)
|
|
274
|
+
|
|
275
|
+
logger.debug(f"Available resources: {len(resources)}")
|
|
276
|
+
return resources
|
|
277
|
+
|
|
278
|
+
async def _create_prompts_from_connectors(self, connectors: list[BaseConnector]) -> list[T]:
|
|
279
|
+
"""Create prompts from MCP prompts in all provided connectors."""
|
|
280
|
+
prompts = []
|
|
281
|
+
for connector in connectors:
|
|
282
|
+
connector_prompts = await self.load_prompts_for_connector(connector)
|
|
283
|
+
prompts.extend(connector_prompts)
|
|
284
|
+
|
|
285
|
+
logger.debug(f"Available prompts: {len(prompts)}")
|
|
286
|
+
return prompts
|
|
287
|
+
|
|
288
|
+
def _check_connector_initialized(self, connector: BaseConnector) -> bool:
|
|
289
|
+
"""Check if a connector is initialized and has tools.
|
|
290
|
+
|
|
291
|
+
Args:
|
|
292
|
+
connector: The connector to check.
|
|
293
|
+
|
|
294
|
+
Returns:
|
|
295
|
+
True if the connector is initialized and has tools, False otherwise.
|
|
296
|
+
"""
|
|
297
|
+
return hasattr(connector, "tools") and connector.tools
|
|
298
|
+
|
|
299
|
+
async def _ensure_connector_initialized(self, connector: BaseConnector) -> bool:
|
|
300
|
+
"""Ensure a connector is initialized.
|
|
301
|
+
|
|
302
|
+
Args:
|
|
303
|
+
connector: The connector to initialize.
|
|
304
|
+
|
|
305
|
+
Returns:
|
|
306
|
+
True if initialization succeeded, False otherwise.
|
|
307
|
+
"""
|
|
308
|
+
if not self._check_connector_initialized(connector):
|
|
309
|
+
logger.debug("Connector doesn't have tools, initializing it")
|
|
310
|
+
try:
|
|
311
|
+
await connector.initialize()
|
|
312
|
+
return True
|
|
313
|
+
except Exception as e:
|
|
314
|
+
logger.error(f"Error initializing connector: {e}")
|
|
315
|
+
return False
|
|
316
|
+
return True
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from collections.abc import Callable, Coroutine
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from mcp.types import Prompt, Resource, Tool
|
|
6
|
+
|
|
7
|
+
from mcp_use.agents.adapters.base import BaseAdapter
|
|
8
|
+
from mcp_use.client.connectors.base import BaseConnector
|
|
9
|
+
|
|
10
|
+
try:
|
|
11
|
+
from google.genai import types
|
|
12
|
+
except ImportError as e:
|
|
13
|
+
raise ImportError(
|
|
14
|
+
"google-genai is required for GoogleMCPAdapter. Install it with: uv pip install google-genai"
|
|
15
|
+
) from e
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _sanitize_for_tool_name(name: str) -> str:
|
|
19
|
+
"""Sanitizes a string to be a valid tool name for Google."""
|
|
20
|
+
# Google tool names can only contain a-z, A-Z, 0-9, and underscores,
|
|
21
|
+
# and must be 64 characters or less.
|
|
22
|
+
return re.sub(r"[^a-zA-Z0-9_]+", "_", name).strip("_")[:64]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class GoogleMCPAdapter(BaseAdapter):
|
|
26
|
+
def __init__(self, disallowed_tools: list[str] | None = None) -> None:
|
|
27
|
+
"""Initialize a new Google adapter.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
disallowed_tools: list of tool names that should not be available.
|
|
31
|
+
"""
|
|
32
|
+
super().__init__(disallowed_tools)
|
|
33
|
+
# This map stores the actual async function to call for each tool.
|
|
34
|
+
self.tool_executors: dict[str, Callable[..., Coroutine[Any, Any, Any]]] = {}
|
|
35
|
+
|
|
36
|
+
self._connector_tool_map: dict[BaseConnector, list[types.FunctionDeclaration]] = {}
|
|
37
|
+
self._connector_resource_map: dict[BaseConnector, list[types.FunctionDeclaration]] = {}
|
|
38
|
+
self._connector_prompt_map: dict[BaseConnector, list[types.FunctionDeclaration]] = {}
|
|
39
|
+
|
|
40
|
+
self.tools: list[types.FunctionDeclaration] = []
|
|
41
|
+
self.resources: list[types.FunctionDeclaration] = []
|
|
42
|
+
self.prompts: list[types.FunctionDeclaration] = []
|
|
43
|
+
|
|
44
|
+
def _convert_tool(self, mcp_tool: Tool, connector: BaseConnector) -> types.FunctionDeclaration:
|
|
45
|
+
"""Convert an MCP tool to the Google tool format."""
|
|
46
|
+
if mcp_tool.name in self.disallowed_tools:
|
|
47
|
+
return None
|
|
48
|
+
|
|
49
|
+
self.tool_executors[mcp_tool.name] = (
|
|
50
|
+
lambda connector=connector, name=mcp_tool.name, **kwargs: connector.call_tool(name, kwargs)
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
fixed_schema = self.fix_schema(mcp_tool.inputSchema)
|
|
54
|
+
function_declaration = types.FunctionDeclaration(
|
|
55
|
+
name=mcp_tool.name, description=mcp_tool.description, parameters_json_schema=fixed_schema
|
|
56
|
+
)
|
|
57
|
+
return function_declaration
|
|
58
|
+
|
|
59
|
+
def _convert_resource(self, mcp_resource: Resource, connector: BaseConnector) -> types.FunctionDeclaration:
|
|
60
|
+
"""Convert an MCP resource to a readable tool in Google format."""
|
|
61
|
+
tool_name = _sanitize_for_tool_name(f"resource_{mcp_resource.name}")
|
|
62
|
+
|
|
63
|
+
if tool_name in self.disallowed_tools:
|
|
64
|
+
return None
|
|
65
|
+
|
|
66
|
+
self.tool_executors[tool_name] = (
|
|
67
|
+
lambda connector=connector, uri=mcp_resource.uri, **kwargs: connector.read_resource(uri)
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
function_declaration = types.FunctionDeclaration(
|
|
71
|
+
name=tool_name,
|
|
72
|
+
description=mcp_resource.description,
|
|
73
|
+
parameters_json_schema={"input_schema": {"type": "object", "properties": {}}},
|
|
74
|
+
)
|
|
75
|
+
return function_declaration
|
|
76
|
+
|
|
77
|
+
def _convert_prompt(self, mcp_prompt: Prompt, connector: BaseConnector) -> types.FunctionDeclaration:
|
|
78
|
+
"""Convert an MCP prompt to a usable tool in Google format."""
|
|
79
|
+
if mcp_prompt.name in self.disallowed_tools:
|
|
80
|
+
return None
|
|
81
|
+
|
|
82
|
+
self.tool_executors[mcp_prompt.name] = (
|
|
83
|
+
lambda connector=connector, name=mcp_prompt.name, **kwargs: connector.get_prompt(name, kwargs)
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
properties = {}
|
|
87
|
+
required_args = []
|
|
88
|
+
if mcp_prompt.arguments:
|
|
89
|
+
for arg in mcp_prompt.arguments:
|
|
90
|
+
prop = {"type": "string"}
|
|
91
|
+
if arg.description:
|
|
92
|
+
prop["description"] = arg.description
|
|
93
|
+
properties[arg.name] = prop
|
|
94
|
+
if arg.required:
|
|
95
|
+
required_args.append(arg.name)
|
|
96
|
+
parameters_schema = {"type": "object", "properties": properties}
|
|
97
|
+
if required_args:
|
|
98
|
+
parameters_schema["required"] = required_args
|
|
99
|
+
|
|
100
|
+
function_declaration = types.FunctionDeclaration(
|
|
101
|
+
name=mcp_prompt.name, description=mcp_prompt.description, parameters_json_schema=parameters_schema
|
|
102
|
+
)
|
|
103
|
+
return function_declaration
|
|
@@ -0,0 +1,212 @@
|
|
|
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
|
+
self._connector_resource_map: dict[BaseConnector, list[BaseTool]] = {}
|
|
39
|
+
self._connector_prompt_map: dict[BaseConnector, list[BaseTool]] = {}
|
|
40
|
+
|
|
41
|
+
self.tools: list[BaseTool] = []
|
|
42
|
+
self.resources: list[BaseTool] = []
|
|
43
|
+
self.prompts: list[BaseTool] = []
|
|
44
|
+
|
|
45
|
+
@telemetry("adapter_convert_tool")
|
|
46
|
+
def _convert_tool(self, mcp_tool: dict[str, Any], connector: BaseConnector) -> BaseTool:
|
|
47
|
+
"""Convert an MCP tool to LangChain's tool format.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
mcp_tool: The MCP tool to convert.
|
|
51
|
+
connector: The connector that provides this tool.
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
A LangChain BaseTool.
|
|
55
|
+
"""
|
|
56
|
+
# Skip disallowed tools
|
|
57
|
+
if mcp_tool.name in self.disallowed_tools:
|
|
58
|
+
return None
|
|
59
|
+
|
|
60
|
+
# This is a dynamic class creation, we need to work with the self reference
|
|
61
|
+
adapter_self = self
|
|
62
|
+
|
|
63
|
+
class McpToLangChainAdapter(BaseTool):
|
|
64
|
+
name: str = mcp_tool.name or "NO NAME"
|
|
65
|
+
description: str = mcp_tool.description or ""
|
|
66
|
+
# Convert JSON schema to Pydantic model for argument validation
|
|
67
|
+
args_schema: type[BaseModel] = jsonschema_to_pydantic(
|
|
68
|
+
adapter_self.fix_schema(mcp_tool.inputSchema) # Apply schema conversion
|
|
69
|
+
)
|
|
70
|
+
tool_connector: BaseConnector = connector # Renamed variable to avoid name conflict
|
|
71
|
+
handle_tool_error: bool = True
|
|
72
|
+
|
|
73
|
+
def __repr__(self) -> str:
|
|
74
|
+
return f"MCP tool: {self.name}: {self.description}"
|
|
75
|
+
|
|
76
|
+
def _run(self, **kwargs: Any) -> NoReturn:
|
|
77
|
+
"""Synchronous run method that always raises an error.
|
|
78
|
+
|
|
79
|
+
Raises:
|
|
80
|
+
NotImplementedError: Always raises this error because MCP tools
|
|
81
|
+
only support async operations.
|
|
82
|
+
"""
|
|
83
|
+
raise NotImplementedError("MCP tools only support async operations")
|
|
84
|
+
|
|
85
|
+
async def _arun(self, **kwargs: Any) -> str | dict:
|
|
86
|
+
"""Asynchronously execute the tool with given arguments.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
kwargs: The arguments to pass to the tool.
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
The result of the tool execution.
|
|
93
|
+
|
|
94
|
+
Raises:
|
|
95
|
+
ToolException: If tool execution fails.
|
|
96
|
+
"""
|
|
97
|
+
logger.debug(f'MCP tool: "{self.name}" received input: {kwargs}')
|
|
98
|
+
|
|
99
|
+
try:
|
|
100
|
+
tool_result: CallToolResult = await self.tool_connector.call_tool(self.name, kwargs)
|
|
101
|
+
try:
|
|
102
|
+
# Use the helper function to parse the result
|
|
103
|
+
return str(tool_result.content)
|
|
104
|
+
except Exception as e:
|
|
105
|
+
# Log the exception for debugging
|
|
106
|
+
logger.error(f"Error parsing tool result: {e}")
|
|
107
|
+
return format_error(e, tool=self.name, tool_content=tool_result.content)
|
|
108
|
+
|
|
109
|
+
except Exception as e:
|
|
110
|
+
if self.handle_tool_error:
|
|
111
|
+
return format_error(e, tool=self.name) # Format the error to make LLM understand it
|
|
112
|
+
raise
|
|
113
|
+
|
|
114
|
+
return McpToLangChainAdapter()
|
|
115
|
+
|
|
116
|
+
def _convert_resource(self, mcp_resource: Resource, connector: BaseConnector) -> BaseTool:
|
|
117
|
+
"""Convert an MCP resource to LangChain's tool format.
|
|
118
|
+
|
|
119
|
+
Each resource becomes an async tool that returns its content when called.
|
|
120
|
+
The tool takes **no** arguments because the resource URI is fixed.
|
|
121
|
+
"""
|
|
122
|
+
|
|
123
|
+
def _sanitize(name: str) -> str:
|
|
124
|
+
return re.sub(r"[^A-Za-z0-9_]+", "_", name).lower().strip("_")
|
|
125
|
+
|
|
126
|
+
class ResourceTool(BaseTool):
|
|
127
|
+
name: str = _sanitize(mcp_resource.name or f"resource_{mcp_resource.uri}")
|
|
128
|
+
description: str = (
|
|
129
|
+
mcp_resource.description or f"Return the content of the resource located at URI {mcp_resource.uri}."
|
|
130
|
+
)
|
|
131
|
+
args_schema: type[BaseModel] = ReadResourceRequestParams
|
|
132
|
+
tool_connector: BaseConnector = connector
|
|
133
|
+
handle_tool_error: bool = True
|
|
134
|
+
|
|
135
|
+
def _run(self, **kwargs: Any) -> NoReturn:
|
|
136
|
+
raise NotImplementedError("Resource tools only support async operations")
|
|
137
|
+
|
|
138
|
+
async def _arun(self, **kwargs: Any) -> Any:
|
|
139
|
+
logger.debug(f'Resource tool: "{self.name}" called')
|
|
140
|
+
try:
|
|
141
|
+
result = await self.tool_connector.read_resource(mcp_resource.uri)
|
|
142
|
+
for content in result.contents:
|
|
143
|
+
# Attempt to decode bytes if necessary
|
|
144
|
+
if isinstance(content, bytes):
|
|
145
|
+
content_decoded = content.decode()
|
|
146
|
+
else:
|
|
147
|
+
content_decoded = str(content)
|
|
148
|
+
|
|
149
|
+
return content_decoded
|
|
150
|
+
except Exception as e:
|
|
151
|
+
if self.handle_tool_error:
|
|
152
|
+
return format_error(e, tool=self.name) # Format the error to make LLM understand it
|
|
153
|
+
raise
|
|
154
|
+
|
|
155
|
+
return ResourceTool()
|
|
156
|
+
|
|
157
|
+
def _convert_prompt(self, mcp_prompt: Prompt, connector: BaseConnector) -> BaseTool:
|
|
158
|
+
"""Convert an MCP prompt to LangChain's tool format.
|
|
159
|
+
|
|
160
|
+
The resulting tool executes `get_prompt` on the connector with the prompt's name and
|
|
161
|
+
the user-provided arguments (if any). The tool returns the decoded prompt content.
|
|
162
|
+
"""
|
|
163
|
+
prompt_arguments = mcp_prompt.arguments
|
|
164
|
+
|
|
165
|
+
# Sanitize the prompt name to create a valid Python identifier for the model name
|
|
166
|
+
base_model_name = re.sub(r"[^a-zA-Z0-9_]", "_", mcp_prompt.name)
|
|
167
|
+
if not base_model_name or base_model_name[0].isdigit():
|
|
168
|
+
base_model_name = "PromptArgs_" + base_model_name
|
|
169
|
+
dynamic_model_name = f"{base_model_name}_InputSchema"
|
|
170
|
+
|
|
171
|
+
if prompt_arguments:
|
|
172
|
+
field_definitions_for_create: dict[str, Any] = {}
|
|
173
|
+
for arg in prompt_arguments:
|
|
174
|
+
param_type: type = getattr(arg, "type", str)
|
|
175
|
+
if arg.required:
|
|
176
|
+
field_definitions_for_create[arg.name] = (
|
|
177
|
+
param_type,
|
|
178
|
+
Field(description=arg.description),
|
|
179
|
+
)
|
|
180
|
+
else:
|
|
181
|
+
field_definitions_for_create[arg.name] = (
|
|
182
|
+
param_type | None,
|
|
183
|
+
Field(None, description=arg.description),
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
InputSchema = create_model(dynamic_model_name, **field_definitions_for_create, __base__=BaseModel)
|
|
187
|
+
else:
|
|
188
|
+
# Create an empty Pydantic model if there are no arguments
|
|
189
|
+
InputSchema = create_model(dynamic_model_name, __base__=BaseModel)
|
|
190
|
+
|
|
191
|
+
class PromptTool(BaseTool):
|
|
192
|
+
name: str = mcp_prompt.name
|
|
193
|
+
description: str = mcp_prompt.description
|
|
194
|
+
|
|
195
|
+
args_schema: type[BaseModel] = InputSchema
|
|
196
|
+
tool_connector: BaseConnector = connector
|
|
197
|
+
handle_tool_error: bool = True
|
|
198
|
+
|
|
199
|
+
def _run(self, **kwargs: Any) -> NoReturn:
|
|
200
|
+
raise NotImplementedError("Prompt tools only support async operations")
|
|
201
|
+
|
|
202
|
+
async def _arun(self, **kwargs: Any) -> Any:
|
|
203
|
+
logger.debug(f'Prompt tool: "{self.name}" called with args: {kwargs}')
|
|
204
|
+
try:
|
|
205
|
+
result = await self.tool_connector.get_prompt(self.name, kwargs)
|
|
206
|
+
return result.messages
|
|
207
|
+
except Exception as e:
|
|
208
|
+
if self.handle_tool_error:
|
|
209
|
+
return format_error(e, tool=self.name) # Format the error to make LLM understand it
|
|
210
|
+
raise
|
|
211
|
+
|
|
212
|
+
return PromptTool()
|