langchain-mcp-tools 0.0.13__py3-none-any.whl → 0.0.15__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-0.0.13.dist-info → langchain_mcp_tools-0.0.15.dist-info}/METADATA +12 -11
- langchain_mcp_tools-0.0.15.dist-info/RECORD +10 -0
- langchain_mcp_tools-0.0.15.dist-info/top_level.txt +1 -0
- {langchain_mcp_tools → src/langchain_mcp_tools}/langchain_mcp_tools.py +10 -8
- src/tests/__init__.py +2 -0
- src/tests/test_langchain_mcp_tools.py +163 -0
- langchain_mcp_tools-0.0.13.dist-info/RECORD +0 -8
- langchain_mcp_tools-0.0.13.dist-info/top_level.txt +0 -1
- {langchain_mcp_tools-0.0.13.dist-info → langchain_mcp_tools-0.0.15.dist-info}/LICENSE +0 -0
- {langchain_mcp_tools-0.0.13.dist-info → langchain_mcp_tools-0.0.15.dist-info}/WHEEL +0 -0
- {langchain_mcp_tools → src/langchain_mcp_tools}/__init__.py +0 -0
- {langchain_mcp_tools → src/langchain_mcp_tools}/py.typed +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.2
|
2
2
|
Name: langchain-mcp-tools
|
3
|
-
Version: 0.0.
|
3
|
+
Version: 0.0.15
|
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
|
@@ -108,7 +108,7 @@ Currently, only text results of tool calls are supported.
|
|
108
108
|
## Technical Details
|
109
109
|
|
110
110
|
It was very tricky (for me) to get the parallel MCP server initialization
|
111
|
-
to work
|
111
|
+
to work, including successful final resource cleanup...
|
112
112
|
|
113
113
|
I'm new to Python, so it is very possible that my ignorance is playing
|
114
114
|
a big role here...
|
@@ -119,23 +119,24 @@ Any comments pointing out something I am missing would be greatly appreciated!
|
|
119
119
|
[(comment here)](https://github.com/hideya/langchain-mcp-tools-ts/issues)
|
120
120
|
|
121
121
|
1. Core Challenge:
|
122
|
-
|
122
|
+
|
123
|
+
A key requirement for parallel initialization is that each server must be
|
124
|
+
initialized in its own dedicated task - there's no way around this as far as
|
125
|
+
I know. However, this poses a challenge when combined with
|
126
|
+
`asynccontextmanager`.
|
127
|
+
|
128
|
+
- Resources management for `stdio_client` and `ClientSession` seems
|
123
129
|
to require relying exclusively on `asynccontextmanager` for cleanup,
|
124
130
|
with no manual cleanup options
|
125
131
|
(based on [the mcp python-sdk impl as of Jan 14, 2025](https://github.com/modelcontextprotocol/python-sdk/tree/99727a9/src/mcp/client))
|
126
132
|
- Initializing multiple MCP servers in parallel requires a dedicated
|
127
133
|
`asyncio.Task` per server
|
128
|
-
-
|
134
|
+
- Need to keep sessions alive for later use by different tasks
|
129
135
|
after initialization
|
130
|
-
-
|
136
|
+
- Need to ensure proper cleanup later in the same task that created them
|
131
137
|
|
132
138
|
2. Solution Strategy:
|
133
139
|
|
134
|
-
A key requirement for parallel initialization is that each server must be
|
135
|
-
initialized in its own dedicated task - there's no way around this as far
|
136
|
-
as I understand. However, this creates a challenge since we also need to
|
137
|
-
maintain long-lived sessions and handle cleanup properly.
|
138
|
-
|
139
140
|
The key insight is to keep the initialization tasks alive throughout the
|
140
141
|
session lifetime, rather than letting them complete after initialization.
|
141
142
|
|
@@ -156,7 +157,7 @@ Any comments pointing out something I am missing would be greatly appreciated!
|
|
156
157
|
|
157
158
|
3. Task Lifecycle:
|
158
159
|
|
159
|
-
|
160
|
+
The following illustrates how to implement the above-mentioned strategy:
|
160
161
|
```
|
161
162
|
[Task starts]
|
162
163
|
↓
|
@@ -0,0 +1,10 @@
|
|
1
|
+
src/langchain_mcp_tools/__init__.py,sha256=Xtv2VphhrWB_KlxTIofHZqtCIGtNEl0MxugnrNXTERA,94
|
2
|
+
src/langchain_mcp_tools/langchain_mcp_tools.py,sha256=EJgm28o2M3mBsFkeDtpw6hooSE1oWwjFJYhRfirxCgE,10511
|
3
|
+
src/langchain_mcp_tools/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
4
|
+
src/tests/__init__.py,sha256=1tFqfPgsnrucXkBKUZ5LMAJ1lk5pdnHAFlUNqR_w21E,31
|
5
|
+
src/tests/test_langchain_mcp_tools.py,sha256=oy5fpsvf-p5YV6zPORau_Wz8rmkF-mNWR3T2r03XzjI,4464
|
6
|
+
langchain_mcp_tools-0.0.15.dist-info/LICENSE,sha256=CRC91e8v116gCpnp7h49oIa6_zjhxqnHFTREeoZFJwA,1072
|
7
|
+
langchain_mcp_tools-0.0.15.dist-info/METADATA,sha256=v-caB8b65upCaAni4N6ChUSjr6rGCAV4fwlH-Uy0S5M,6943
|
8
|
+
langchain_mcp_tools-0.0.15.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
|
9
|
+
langchain_mcp_tools-0.0.15.dist-info/top_level.txt,sha256=74rtVfumQlgAPzR5_2CgYN24MB0XARCg0t-gzk6gTrM,4
|
10
|
+
langchain_mcp_tools-0.0.15.dist-info/RECORD,,
|
@@ -0,0 +1 @@
|
|
1
|
+
src
|
@@ -37,21 +37,23 @@ require context managers while enabling parallel initialization.
|
|
37
37
|
The key aspects are:
|
38
38
|
|
39
39
|
1. Core Challenge:
|
40
|
-
|
40
|
+
|
41
|
+
A key requirement for parallel initialization is that each server must be
|
42
|
+
initialized in its own dedicated task - there's no way around this as far as
|
43
|
+
I know. However, this poses a challenge when combined with
|
44
|
+
`asynccontextmanager`.
|
45
|
+
|
46
|
+
- Resources management for `stdio_client` and `ClientSession` seems
|
41
47
|
to require relying exclusively on `asynccontextmanager` for cleanup,
|
42
48
|
with no manual cleanup options
|
43
49
|
(based on [the mcp python-sdk impl as of Jan 14, 2025](https://github.com/modelcontextprotocol/python-sdk/tree/99727a9/src/mcp/client))
|
44
50
|
- Initializing multiple MCP servers in parallel requires a dedicated
|
45
51
|
`asyncio.Task` per server
|
46
|
-
-
|
52
|
+
- Need to keep sessions alive for later use by different tasks
|
47
53
|
after initialization
|
48
|
-
-
|
54
|
+
- Need to ensure proper cleanup later in the same task that created them
|
49
55
|
|
50
56
|
2. Solution Strategy:
|
51
|
-
A key requirement for parallel initialization is that each server must be
|
52
|
-
initialized in its own dedicated task - there's no way around this as far
|
53
|
-
as I understand. However, this creates a challenge since we also need to
|
54
|
-
maintain long-lived sessions and handle cleanup properly.
|
55
57
|
|
56
58
|
The key insight is to keep the initialization tasks alive throughout the
|
57
59
|
session lifetime, rather than letting them complete after initialization.
|
@@ -73,7 +75,7 @@ The key aspects are:
|
|
73
75
|
|
74
76
|
3. Task Lifecycle:
|
75
77
|
|
76
|
-
|
78
|
+
The following illustrates how to implement the above-mentioned strategy:
|
77
79
|
```
|
78
80
|
[Task starts]
|
79
81
|
↓
|
src/tests/__init__.py
ADDED
@@ -0,0 +1,163 @@
|
|
1
|
+
import pytest
|
2
|
+
from unittest.mock import AsyncMock, MagicMock, patch
|
3
|
+
from langchain_core.tools import BaseTool
|
4
|
+
from langchain_mcp_tools.langchain_mcp_tools import (
|
5
|
+
convert_mcp_to_langchain_tools,
|
6
|
+
)
|
7
|
+
|
8
|
+
# Fix the asyncio mark warning by installing pytest-asyncio
|
9
|
+
pytest_plugins = ('pytest_asyncio',)
|
10
|
+
|
11
|
+
|
12
|
+
@pytest.fixture
|
13
|
+
def mock_stdio_client():
|
14
|
+
with patch('langchain_mcp_tools.langchain_mcp_tools.stdio_client') as mock:
|
15
|
+
mock.return_value.__aenter__.return_value = (AsyncMock(), AsyncMock())
|
16
|
+
yield mock
|
17
|
+
|
18
|
+
|
19
|
+
@pytest.fixture
|
20
|
+
def mock_client_session():
|
21
|
+
with patch('langchain_mcp_tools.langchain_mcp_tools.ClientSession') as mock:
|
22
|
+
session = AsyncMock()
|
23
|
+
# Mock the list_tools response
|
24
|
+
session.list_tools.return_value = MagicMock(
|
25
|
+
tools=[
|
26
|
+
MagicMock(
|
27
|
+
name="tool1",
|
28
|
+
description="Test tool",
|
29
|
+
inputSchema={"type": "object", "properties": {}}
|
30
|
+
)
|
31
|
+
]
|
32
|
+
)
|
33
|
+
mock.return_value.__aenter__.return_value = session
|
34
|
+
yield mock
|
35
|
+
|
36
|
+
|
37
|
+
@pytest.mark.asyncio
|
38
|
+
async def test_convert_mcp_to_langchain_tools_empty():
|
39
|
+
server_configs = {}
|
40
|
+
tools, cleanup = await convert_mcp_to_langchain_tools(server_configs)
|
41
|
+
assert isinstance(tools, list)
|
42
|
+
assert len(tools) == 0
|
43
|
+
await cleanup()
|
44
|
+
|
45
|
+
|
46
|
+
"""
|
47
|
+
@pytest.mark.asyncio
|
48
|
+
async def test_convert_mcp_to_langchain_tools_invalid_config():
|
49
|
+
server_configs = {"invalid": {"command": "nonexistent"}}
|
50
|
+
with pytest.raises(Exception):
|
51
|
+
await convert_mcp_to_langchain_tools(server_configs)
|
52
|
+
"""
|
53
|
+
|
54
|
+
|
55
|
+
"""
|
56
|
+
@pytest.mark.asyncio
|
57
|
+
async def test_convert_single_mcp_success(
|
58
|
+
mock_stdio_client,
|
59
|
+
mock_client_session
|
60
|
+
):
|
61
|
+
# Test data
|
62
|
+
server_name = "test_server"
|
63
|
+
server_config = {
|
64
|
+
"command": "test_command",
|
65
|
+
"args": ["--test"],
|
66
|
+
"env": {"TEST_ENV": "value"}
|
67
|
+
}
|
68
|
+
langchain_tools = []
|
69
|
+
ready_event = asyncio.Event()
|
70
|
+
cleanup_event = asyncio.Event()
|
71
|
+
|
72
|
+
# Create task
|
73
|
+
task = asyncio.create_task(
|
74
|
+
convert_single_mcp_to_langchain_tools(
|
75
|
+
server_name,
|
76
|
+
server_config,
|
77
|
+
langchain_tools,
|
78
|
+
ready_event,
|
79
|
+
cleanup_event
|
80
|
+
)
|
81
|
+
)
|
82
|
+
|
83
|
+
# Wait for ready event
|
84
|
+
await asyncio.wait_for(ready_event.wait(), timeout=1.0)
|
85
|
+
|
86
|
+
# Verify tools were created
|
87
|
+
assert len(langchain_tools) == 1
|
88
|
+
assert isinstance(langchain_tools[0], BaseTool)
|
89
|
+
assert langchain_tools[0].name == "tool1"
|
90
|
+
|
91
|
+
# Trigger cleanup
|
92
|
+
cleanup_event.set()
|
93
|
+
await task
|
94
|
+
"""
|
95
|
+
|
96
|
+
|
97
|
+
@pytest.mark.asyncio
|
98
|
+
async def test_convert_mcp_to_langchain_tools_multiple_servers(
|
99
|
+
mock_stdio_client,
|
100
|
+
mock_client_session
|
101
|
+
):
|
102
|
+
server_configs = {
|
103
|
+
"server1": {"command": "cmd1", "args": []},
|
104
|
+
"server2": {"command": "cmd2", "args": []}
|
105
|
+
}
|
106
|
+
|
107
|
+
tools, cleanup = await convert_mcp_to_langchain_tools(server_configs)
|
108
|
+
|
109
|
+
# Verify correct number of tools created
|
110
|
+
assert len(tools) == 2 # One tool per server
|
111
|
+
assert all(isinstance(tool, BaseTool) for tool in tools)
|
112
|
+
|
113
|
+
# Test cleanup
|
114
|
+
await cleanup()
|
115
|
+
|
116
|
+
|
117
|
+
"""
|
118
|
+
@pytest.mark.asyncio
|
119
|
+
async def test_tool_execution(mock_stdio_client, mock_client_session):
|
120
|
+
server_configs = {
|
121
|
+
"test_server": {"command": "test", "args": []}
|
122
|
+
}
|
123
|
+
|
124
|
+
# Mock the tool execution response
|
125
|
+
session = mock_client_session.return_value.__aenter__.return_value
|
126
|
+
session.call_tool.return_value = MagicMock(
|
127
|
+
isError=False,
|
128
|
+
content={"result": "success"}
|
129
|
+
)
|
130
|
+
|
131
|
+
tools, cleanup = await convert_mcp_to_langchain_tools(server_configs)
|
132
|
+
|
133
|
+
# Test tool execution
|
134
|
+
result = await tools[0]._arun(test_param="value")
|
135
|
+
assert result == {"result": "success"}
|
136
|
+
|
137
|
+
# Verify tool was called with correct parameters
|
138
|
+
session.call_tool.assert_called_once_with("tool1", {"test_param": "value"})
|
139
|
+
|
140
|
+
await cleanup()
|
141
|
+
"""
|
142
|
+
|
143
|
+
|
144
|
+
@pytest.mark.asyncio
|
145
|
+
async def test_tool_execution_error(mock_stdio_client, mock_client_session):
|
146
|
+
server_configs = {
|
147
|
+
"test_server": {"command": "test", "args": []}
|
148
|
+
}
|
149
|
+
|
150
|
+
# Mock error response
|
151
|
+
session = mock_client_session.return_value.__aenter__.return_value
|
152
|
+
session.call_tool.return_value = MagicMock(
|
153
|
+
isError=True,
|
154
|
+
content="Error message"
|
155
|
+
)
|
156
|
+
|
157
|
+
tools, cleanup = await convert_mcp_to_langchain_tools(server_configs)
|
158
|
+
|
159
|
+
# Test tool execution error
|
160
|
+
with pytest.raises(Exception):
|
161
|
+
await tools[0]._arun(test_param="value")
|
162
|
+
|
163
|
+
await cleanup()
|
@@ -1,8 +0,0 @@
|
|
1
|
-
langchain_mcp_tools/__init__.py,sha256=Xtv2VphhrWB_KlxTIofHZqtCIGtNEl0MxugnrNXTERA,94
|
2
|
-
langchain_mcp_tools/langchain_mcp_tools.py,sha256=5OKa6xeS54vB-mqH1BTZSB6E8Ykh5Cfu9rXqfCqESOg,10558
|
3
|
-
langchain_mcp_tools/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
4
|
-
langchain_mcp_tools-0.0.13.dist-info/LICENSE,sha256=CRC91e8v116gCpnp7h49oIa6_zjhxqnHFTREeoZFJwA,1072
|
5
|
-
langchain_mcp_tools-0.0.13.dist-info/METADATA,sha256=tJx9mGu94T09xAYPSCB14V1cIp7w31cM1ndNbUev1vQ,6959
|
6
|
-
langchain_mcp_tools-0.0.13.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
|
7
|
-
langchain_mcp_tools-0.0.13.dist-info/top_level.txt,sha256=aR_9V2A1Yt-Bca60KmndmGLUWb2wiM5IOG-Gkaf1dxY,20
|
8
|
-
langchain_mcp_tools-0.0.13.dist-info/RECORD,,
|
@@ -1 +0,0 @@
|
|
1
|
-
langchain_mcp_tools
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|