langchain-mcp-tools 0.2.2__py3-none-any.whl → 0.2.3__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.
- langchain_mcp_tools/langchain_mcp_tools.py +84 -59
- {langchain_mcp_tools-0.2.2.dist-info → langchain_mcp_tools-0.2.3.dist-info}/METADATA +18 -8
- langchain_mcp_tools-0.2.3.dist-info/RECORD +8 -0
- {langchain_mcp_tools-0.2.2.dist-info → langchain_mcp_tools-0.2.3.dist-info}/WHEEL +1 -1
- langchain_mcp_tools-0.2.2.dist-info/RECORD +0 -8
- {langchain_mcp_tools-0.2.2.dist-info → langchain_mcp_tools-0.2.3.dist-info}/licenses/LICENSE +0 -0
- {langchain_mcp_tools-0.2.2.dist-info → langchain_mcp_tools-0.2.3.dist-info}/top_level.txt +0 -0
@@ -32,8 +32,8 @@ try:
|
|
32
32
|
from pydantic import BaseModel
|
33
33
|
# from pydantic_core import to_json
|
34
34
|
except ImportError as e:
|
35
|
-
print(f
|
36
|
-
print(
|
35
|
+
print(f"\nError: Required package not found: {e}")
|
36
|
+
print("Please ensure all required packages are installed\n")
|
37
37
|
sys.exit(1)
|
38
38
|
|
39
39
|
|
@@ -56,11 +56,18 @@ McpServersConfig = dict[str, McpServerConfig]
|
|
56
56
|
|
57
57
|
|
58
58
|
def fix_schema(schema: dict) -> dict:
|
59
|
-
"""Converts JSON Schema
|
59
|
+
"""Converts JSON Schema "type": ["string", "null"] to "anyOf" format.
|
60
|
+
|
61
|
+
Args:
|
62
|
+
schema: A JSON schema dictionary
|
63
|
+
|
64
|
+
Returns:
|
65
|
+
Modified schema with converted type formats
|
66
|
+
"""
|
60
67
|
if isinstance(schema, dict):
|
61
|
-
if
|
62
|
-
schema[
|
63
|
-
del schema[
|
68
|
+
if "type" in schema and isinstance(schema["type"], list):
|
69
|
+
schema["anyOf"] = [{"type": t} for t in schema["type"]]
|
70
|
+
del schema["type"] # Remove "type" and standardize to "anyOf"
|
64
71
|
for key, value in schema.items():
|
65
72
|
schema[key] = fix_schema(value) # Apply recursively
|
66
73
|
return schema
|
@@ -80,8 +87,7 @@ async def spawn_mcp_server_and_get_transport(
|
|
80
87
|
exit_stack: AsyncExitStack,
|
81
88
|
logger: logging.Logger = logging.getLogger(__name__)
|
82
89
|
) -> Transport:
|
83
|
-
"""
|
84
|
-
Spawns an MCP server process and establishes communication channels.
|
90
|
+
"""Spawns an MCP server process and establishes communication channels.
|
85
91
|
|
86
92
|
Args:
|
87
93
|
server_name: Server instance name to use for better logging
|
@@ -97,19 +103,20 @@ async def spawn_mcp_server_and_get_transport(
|
|
97
103
|
"""
|
98
104
|
try:
|
99
105
|
logger.info(f'MCP server "{server_name}": '
|
100
|
-
f
|
106
|
+
f"initializing with: {server_config}")
|
101
107
|
|
102
|
-
url_str = str(server_config.get(
|
103
|
-
headers =
|
108
|
+
url_str = str(server_config.get("url")) # None becomes "None"
|
109
|
+
headers = (cast(McpServerUrlBasedConfig, server_config)
|
110
|
+
.get("headers", None))
|
104
111
|
# no exception thrown even for a malformed URL
|
105
112
|
url_scheme = urlparse(url_str).scheme
|
106
113
|
|
107
|
-
if url_scheme in (
|
114
|
+
if url_scheme in ("http", "https"):
|
108
115
|
transport = await exit_stack.enter_async_context(
|
109
116
|
sse_client(url_str, headers=headers)
|
110
117
|
)
|
111
118
|
|
112
|
-
elif url_scheme in (
|
119
|
+
elif url_scheme in ("ws", "wss"):
|
113
120
|
transport = await exit_stack.enter_async_context(
|
114
121
|
websocket_client(url_str)
|
115
122
|
)
|
@@ -119,21 +126,21 @@ async def spawn_mcp_server_and_get_transport(
|
|
119
126
|
# To avoid confusion, it was decided to automatically append it
|
120
127
|
# to the env if not explicitly set by the config.
|
121
128
|
config = cast(McpServerCommandBasedConfig, server_config)
|
122
|
-
# env = config.get(
|
123
|
-
env_val = config.get(
|
129
|
+
# env = config.get("env", {}) does't work since it can yield None
|
130
|
+
env_val = config.get("env")
|
124
131
|
env = {} if env_val is None else dict(env_val)
|
125
|
-
if
|
126
|
-
env[
|
132
|
+
if "PATH" not in env:
|
133
|
+
env["PATH"] = os.environ.get("PATH", "")
|
127
134
|
|
128
135
|
# Use stdio client for commands
|
129
|
-
# args = config.get(
|
130
|
-
args_val = config.get(
|
136
|
+
# args = config.get("args", []) does't work since it can yield None
|
137
|
+
args_val = config.get("args")
|
131
138
|
args = [] if args_val is None else list(args_val)
|
132
139
|
server_parameters = StdioServerParameters(
|
133
|
-
command=config.get(
|
140
|
+
command=config.get("command", ""),
|
134
141
|
args=args,
|
135
142
|
env=env,
|
136
|
-
cwd=config.get(
|
143
|
+
cwd=config.get("cwd", None)
|
137
144
|
)
|
138
145
|
|
139
146
|
# Initialize stdio client and register it with exit stack for
|
@@ -145,13 +152,14 @@ async def spawn_mcp_server_and_get_transport(
|
|
145
152
|
# `errlog: TextIO`. I once included `stderr: int` for
|
146
153
|
# compatibility with the TypeScript version, but decided to
|
147
154
|
# follow the Python SDK more closely.
|
148
|
-
errlog_val = server_config
|
149
|
-
|
155
|
+
errlog_val = (cast(McpServerCommandBasedConfig, server_config)
|
156
|
+
.get("errlog"))
|
157
|
+
kwargs = {"errlog": errlog_val} if errlog_val is not None else {}
|
150
158
|
transport = await exit_stack.enter_async_context(
|
151
159
|
stdio_client(server_parameters, **kwargs)
|
152
160
|
)
|
153
161
|
except Exception as e:
|
154
|
-
logger.error(f
|
162
|
+
logger.error(f"Error spawning MCP server: {str(e)}")
|
155
163
|
raise
|
156
164
|
|
157
165
|
return transport
|
@@ -163,8 +171,7 @@ async def get_mcp_server_tools(
|
|
163
171
|
exit_stack: AsyncExitStack,
|
164
172
|
logger: logging.Logger = logging.getLogger(__name__)
|
165
173
|
) -> list[BaseTool]:
|
166
|
-
"""
|
167
|
-
Retrieves and converts MCP server tools to LangChain format.
|
174
|
+
"""Retrieves and converts MCP server tools to LangChain format.
|
168
175
|
|
169
176
|
Args:
|
170
177
|
server_name: Server instance name to use for better logging
|
@@ -211,8 +218,8 @@ async def get_mcp_server_tools(
|
|
211
218
|
|
212
219
|
# Define adapter class to convert MCP tool to LangChain format
|
213
220
|
class McpToLangChainAdapter(BaseTool):
|
214
|
-
name: str = tool.name or
|
215
|
-
description: str = tool.description or
|
221
|
+
name: str = tool.name or "NO NAME"
|
222
|
+
description: str = tool.description or ""
|
216
223
|
# Convert JSON schema to Pydantic model for argument validation
|
217
224
|
args_schema: type[BaseModel] = jsonschema_to_pydantic(
|
218
225
|
fix_schema(tool.inputSchema) # Apply schema conversion
|
@@ -221,31 +228,40 @@ async def get_mcp_server_tools(
|
|
221
228
|
|
222
229
|
def _run(self, **kwargs: Any) -> NoReturn:
|
223
230
|
raise NotImplementedError(
|
224
|
-
|
231
|
+
"MCP tools only support async operations"
|
225
232
|
)
|
226
233
|
|
227
234
|
async def _arun(self, **kwargs: Any) -> Any:
|
228
|
-
"""
|
229
|
-
|
235
|
+
"""Asynchronously executes the tool with given arguments.
|
236
|
+
|
230
237
|
Logs input/output and handles errors.
|
238
|
+
|
239
|
+
Args:
|
240
|
+
**kwargs: Arguments to be passed to the MCP tool
|
241
|
+
|
242
|
+
Returns:
|
243
|
+
Formatted response from the MCP tool as a string
|
244
|
+
|
245
|
+
Raises:
|
246
|
+
ToolException: If the tool execution fails
|
231
247
|
"""
|
232
248
|
logger.info(f'MCP tool "{server_name}"/"{tool.name}" '
|
233
|
-
f
|
249
|
+
f"received input: {kwargs}")
|
234
250
|
|
235
251
|
try:
|
236
252
|
result = await session.call_tool(self.name, kwargs)
|
237
253
|
|
238
|
-
if hasattr(result,
|
254
|
+
if hasattr(result, "isError") and result.isError:
|
239
255
|
raise ToolException(
|
240
|
-
f
|
256
|
+
f"Tool execution failed: {result.content}"
|
241
257
|
)
|
242
258
|
|
243
|
-
if not hasattr(result,
|
259
|
+
if not hasattr(result, "content"):
|
244
260
|
return str(result)
|
245
261
|
|
246
262
|
# The return type of `BaseTool`'s `arun` is `str`.
|
247
263
|
try:
|
248
|
-
result_content_text =
|
264
|
+
result_content_text = "\n\n".join(
|
249
265
|
item.text
|
250
266
|
for item in result.content
|
251
267
|
if isinstance(item, mcp_types.TextContent)
|
@@ -259,20 +275,20 @@ async def get_mcp_server_tools(
|
|
259
275
|
|
260
276
|
except KeyError as e:
|
261
277
|
result_content_text = (
|
262
|
-
f
|
263
|
-
f
|
278
|
+
f"Error in parsing result.content: {str(e)}; "
|
279
|
+
f"contents: {repr(result.content)}"
|
264
280
|
)
|
265
281
|
|
266
282
|
# Log rough result size for monitoring
|
267
283
|
size = len(result_content_text.encode())
|
268
284
|
logger.info(f'MCP tool "{server_name}"/"{tool.name}" '
|
269
|
-
f
|
285
|
+
f"received result (size: {size})")
|
270
286
|
|
271
287
|
# If no text content, return a clear message
|
272
288
|
# describing the situation.
|
273
289
|
result_content_text = (
|
274
290
|
result_content_text or
|
275
|
-
|
291
|
+
"No text content available in response"
|
276
292
|
)
|
277
293
|
|
278
294
|
return result_content_text
|
@@ -280,21 +296,21 @@ async def get_mcp_server_tools(
|
|
280
296
|
except Exception as e:
|
281
297
|
logger.warn(
|
282
298
|
f'MCP tool "{server_name}"/"{tool.name}" '
|
283
|
-
f
|
299
|
+
f"caused error: {str(e)}"
|
284
300
|
)
|
285
301
|
if self.handle_tool_error:
|
286
|
-
return f
|
302
|
+
return f"Error executing MCP tool: {str(e)}"
|
287
303
|
raise
|
288
304
|
|
289
305
|
langchain_tools.append(McpToLangChainAdapter())
|
290
306
|
|
291
307
|
# Log available tools for debugging
|
292
308
|
logger.info(f'MCP server "{server_name}": {len(langchain_tools)} '
|
293
|
-
f
|
309
|
+
f"tool(s) available:")
|
294
310
|
for tool in langchain_tools:
|
295
|
-
logger.info(f
|
311
|
+
logger.info(f"- {tool.name}")
|
296
312
|
except Exception as e:
|
297
|
-
logger.error(f
|
313
|
+
logger.error(f"Error getting MCP tools: {str(e)}")
|
298
314
|
raise
|
299
315
|
|
300
316
|
return langchain_tools
|
@@ -302,9 +318,14 @@ async def get_mcp_server_tools(
|
|
302
318
|
|
303
319
|
# A very simple pre-configured logger for fallback
|
304
320
|
def init_logger() -> logging.Logger:
|
321
|
+
"""Creates a simple pre-configured logger.
|
322
|
+
|
323
|
+
Returns:
|
324
|
+
A configured Logger instance
|
325
|
+
"""
|
305
326
|
logging.basicConfig(
|
306
327
|
level=logging.INFO, # logging.DEBUG,
|
307
|
-
format=
|
328
|
+
format="\x1b[90m[%(levelname)s]\x1b[0m %(message)s"
|
308
329
|
)
|
309
330
|
return logging.getLogger()
|
310
331
|
|
@@ -329,26 +350,30 @@ async def convert_mcp_to_langchain_tools(
|
|
329
350
|
configurations, where each configuration contains command, args,
|
330
351
|
and env settings
|
331
352
|
logger: Logger instance to use for logging events and errors.
|
332
|
-
|
333
|
-
|
353
|
+
If None, uses module logger with fallback to a pre-configured
|
354
|
+
logger when no root handlers exist.
|
334
355
|
|
335
356
|
Returns:
|
336
357
|
A tuple containing:
|
337
|
-
|
338
|
-
|
339
|
-
|
358
|
+
|
359
|
+
* List of converted LangChain tools from all servers
|
360
|
+
* Async cleanup function to properly shutdown all server connections
|
340
361
|
|
341
362
|
Example:
|
363
|
+
|
342
364
|
server_configs = {
|
343
|
-
|
344
|
-
|
365
|
+
"fetch": {
|
366
|
+
"command": "uvx", "args": ["mcp-server-fetch"]
|
345
367
|
},
|
346
|
-
|
347
|
-
|
368
|
+
"weather": {
|
369
|
+
"command": "npx", "args": ["-y","@h1deya/mcp-server-weather"]
|
348
370
|
}
|
349
371
|
}
|
372
|
+
|
350
373
|
tools, cleanup = await convert_mcp_to_langchain_tools(server_configs)
|
374
|
+
|
351
375
|
# Use tools...
|
376
|
+
|
352
377
|
await cleanup()
|
353
378
|
"""
|
354
379
|
|
@@ -394,13 +419,13 @@ async def convert_mcp_to_langchain_tools(
|
|
394
419
|
|
395
420
|
# Define a cleanup function to properly shut down all servers
|
396
421
|
async def mcp_cleanup() -> None:
|
397
|
-
"""Closes all server connections and cleans up resources"""
|
422
|
+
"""Closes all server connections and cleans up resources."""
|
398
423
|
await async_exit_stack.aclose()
|
399
424
|
|
400
425
|
# Log summary of initialized tools
|
401
|
-
logger.info(f
|
402
|
-
f
|
426
|
+
logger.info(f"MCP servers initialized: {len(langchain_tools)} tool(s) "
|
427
|
+
f"available in total")
|
403
428
|
for tool in langchain_tools:
|
404
|
-
logger.debug(f
|
429
|
+
logger.debug(f"- {tool.name}")
|
405
430
|
|
406
431
|
return langchain_tools, mcp_cleanup
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: langchain-mcp-tools
|
3
|
-
Version: 0.2.
|
3
|
+
Version: 0.2.3
|
4
4
|
Summary: Model Context Protocol (MCP) To LangChain Tools Conversion Utility
|
5
5
|
Project-URL: Bug Tracker, https://github.com/hideya/langchain-mcp-tools-py/issues
|
6
6
|
Project-URL: Source Code, https://github.com/hideya/langchain-mcp-tools-py
|
@@ -11,16 +11,17 @@ License-File: LICENSE
|
|
11
11
|
Requires-Dist: jsonschema-pydantic>=0.6
|
12
12
|
Requires-Dist: langchain>=0.3.14
|
13
13
|
Requires-Dist: mcp>=1.6.0
|
14
|
-
Requires-Dist: pyjson5>=1.6.8
|
15
|
-
Requires-Dist: websockets>=15.0.1
|
16
14
|
Provides-Extra: dev
|
17
15
|
Requires-Dist: dotenv>=0.9.9; extra == "dev"
|
16
|
+
Requires-Dist: fastapi>=0.115.12; extra == "dev"
|
17
|
+
Requires-Dist: pyjwt>=2.10.1; extra == "dev"
|
18
18
|
Requires-Dist: langchain-anthropic>=0.3.1; extra == "dev"
|
19
19
|
Requires-Dist: langchain-groq>=0.2.3; extra == "dev"
|
20
20
|
Requires-Dist: langchain-openai>=0.3.0; extra == "dev"
|
21
21
|
Requires-Dist: langgraph>=0.2.62; extra == "dev"
|
22
22
|
Requires-Dist: pytest>=8.3.4; extra == "dev"
|
23
23
|
Requires-Dist: pytest-asyncio>=0.25.2; extra == "dev"
|
24
|
+
Requires-Dist: websockets>=15.0.1; extra == "dev"
|
24
25
|
Dynamic: license-file
|
25
26
|
|
26
27
|
# MCP To LangChain Tools Conversion Utility [](https://github.com/hideya/langchain-mcp-tools-py/blob/main/LICENSE) [](https://pypi.org/project/langchain-mcp-tools/)
|
@@ -67,6 +68,11 @@ A typescript equivalent of this utility is available
|
|
67
68
|
pip install langchain-mcp-tools
|
68
69
|
```
|
69
70
|
|
71
|
+
## API docs
|
72
|
+
|
73
|
+
Can be found [here](https://hideya.github.io/langchain-mcp-tools-py/)
|
74
|
+
|
75
|
+
|
70
76
|
## Quick Start
|
71
77
|
|
72
78
|
A minimal but complete working usage example can be found
|
@@ -145,9 +151,9 @@ Note that the key `"url"` may be changed in the future to match
|
|
145
151
|
the MCP server configurations used by Claude for Desktop once
|
146
152
|
it introduces remote server support.
|
147
153
|
|
148
|
-
A usage example can be found [here](https://github.com/hideya/langchain-mcp-tools-py-usage/blob/
|
154
|
+
A usage example can be found [here](https://github.com/hideya/langchain-mcp-tools-py-usage/blob/3bd35d9fb49f4b631fe3d0cc8491d43cbf69693b/src/example.py#L43-L54)
|
149
155
|
|
150
|
-
###
|
156
|
+
### Authentication Support for SSE Connections
|
151
157
|
|
152
158
|
A new key `"headers"` has been introduced to pass HTTP headers to the SSE (Server-Sent Events) connection.
|
153
159
|
It takes `dict[str, str]` and is primarily intended to support SSE MCP servers
|
@@ -163,6 +169,11 @@ that require authentication via bearer tokens or other custom headers.
|
|
163
169
|
The key name `header` is derived from the Python SDK
|
164
170
|
[`sse_client()`](https://github.com/modelcontextprotocol/python-sdk/blob/babb477dffa33f46cdc886bc885eb1d521151430/src/mcp/client/sse.py#L24) argument name.
|
165
171
|
|
172
|
+
A simple example showing how to implement MCP SSE server and client with authentication can be found
|
173
|
+
in [sse-auth-test-client.py](https://github.com/hideya/langchain-mcp-tools-py-usage/tree/main/src/sse-auth-test-client.py)
|
174
|
+
and in [sse-auth-test-server.py](https://github.com/hideya/langchain-mcp-tools-py-usage/tree/main/src/sse-auth-test-server.py)
|
175
|
+
of [this usage examples repo](https://github.com/hideya/langchain-mcp-tools-py-usage).
|
176
|
+
|
166
177
|
### Working Directory Configuration for Local MCP Servers
|
167
178
|
|
168
179
|
The working directory that is used when spawning a local (stdio) MCP server
|
@@ -179,7 +190,7 @@ can be specified with the `"cwd"` key as follows:
|
|
179
190
|
The key name `cwd` is derived from
|
180
191
|
Python SDK's [`StdioServerParameters`](https://github.com/modelcontextprotocol/python-sdk/blob/babb477dffa33f46cdc886bc885eb1d521151430/src/mcp/client/stdio/__init__.py#L76-L77).
|
181
192
|
|
182
|
-
###
|
193
|
+
### stderr Redirection for Local MCP Server
|
183
194
|
|
184
195
|
A new key `"errlog"` has been introduced to specify a file-like object
|
185
196
|
to which local (stdio) MCP server's stderr is redirected.
|
@@ -190,8 +201,7 @@ to which local (stdio) MCP server's stderr is redirected.
|
|
190
201
|
mcp_servers[server_name]["errlog"] = log_file
|
191
202
|
```
|
192
203
|
|
193
|
-
A usage example can be found [here](
|
194
|
-
https://github.com/hideya/langchain-mcp-tools-py-usage/blob/cf96ddc43750708ef3b244bad95714f0f2fe1d28/src/example.py#L91-L108)
|
204
|
+
A usage example can be found [here](https://github.com/hideya/langchain-mcp-tools-py-usage/blob/3bd35d9fb49f4b631fe3d0cc8491d43cbf69693b/src/example.py#L88-L108)
|
195
205
|
|
196
206
|
**NOTE: Why the key name `errlog` was chosen:**
|
197
207
|
Unlike TypeScript SDK's `StdioServerParameters`, the Python
|
@@ -0,0 +1,8 @@
|
|
1
|
+
langchain_mcp_tools/__init__.py,sha256=iatHG2fCpz143wgQUZpyFVilgri4yOh2P0vxr22gguE,114
|
2
|
+
langchain_mcp_tools/langchain_mcp_tools.py,sha256=3XfJUW98usqrWmn1RqbVzVr_Z4piv8PLzAm29YVj6z4,15806
|
3
|
+
langchain_mcp_tools/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
4
|
+
langchain_mcp_tools-0.2.3.dist-info/licenses/LICENSE,sha256=CRC91e8v116gCpnp7h49oIa6_zjhxqnHFTREeoZFJwA,1072
|
5
|
+
langchain_mcp_tools-0.2.3.dist-info/METADATA,sha256=PPZz0kgmD2JQfUntk4D9R1gcm5DZZ_utV6L0YRo3b-M,9049
|
6
|
+
langchain_mcp_tools-0.2.3.dist-info/WHEEL,sha256=pxyMxgL8-pra_rKaQ4drOZAegBVuX-G_4nRHjjgWbmo,91
|
7
|
+
langchain_mcp_tools-0.2.3.dist-info/top_level.txt,sha256=aR_9V2A1Yt-Bca60KmndmGLUWb2wiM5IOG-Gkaf1dxY,20
|
8
|
+
langchain_mcp_tools-0.2.3.dist-info/RECORD,,
|
@@ -1,8 +0,0 @@
|
|
1
|
-
langchain_mcp_tools/__init__.py,sha256=iatHG2fCpz143wgQUZpyFVilgri4yOh2P0vxr22gguE,114
|
2
|
-
langchain_mcp_tools/langchain_mcp_tools.py,sha256=txbtjl7BAjZu1ccNUNhLkLd3HNdCSfKeyTCDX_0PUR0,15078
|
3
|
-
langchain_mcp_tools/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
4
|
-
langchain_mcp_tools-0.2.2.dist-info/licenses/LICENSE,sha256=CRC91e8v116gCpnp7h49oIa6_zjhxqnHFTREeoZFJwA,1072
|
5
|
-
langchain_mcp_tools-0.2.2.dist-info/METADATA,sha256=hxlEr_nyHbnjLMzhxYOsT_rZQ8-j4rTi7EDDUxyvb-A,8458
|
6
|
-
langchain_mcp_tools-0.2.2.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
|
7
|
-
langchain_mcp_tools-0.2.2.dist-info/top_level.txt,sha256=aR_9V2A1Yt-Bca60KmndmGLUWb2wiM5IOG-Gkaf1dxY,20
|
8
|
-
langchain_mcp_tools-0.2.2.dist-info/RECORD,,
|
{langchain_mcp_tools-0.2.2.dist-info → langchain_mcp_tools-0.2.3.dist-info}/licenses/LICENSE
RENAMED
File without changes
|
File without changes
|