langchain-mcp-tools 0.1.11__py3-none-any.whl → 0.2.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.
@@ -2,4 +2,5 @@
2
2
  from .langchain_mcp_tools import (
3
3
  convert_mcp_to_langchain_tools,
4
4
  McpServerCleanupFn,
5
+ McpServersConfig,
5
6
  )
@@ -1,8 +1,4 @@
1
1
  # Standard library imports
2
- from anyio.streams.memory import (
3
- MemoryObjectReceiveStream,
4
- MemoryObjectSendStream,
5
- )
6
2
  import logging
7
3
  import os
8
4
  import sys
@@ -11,21 +7,27 @@ from typing import (
11
7
  Any,
12
8
  Awaitable,
13
9
  Callable,
14
- Dict,
15
- List,
10
+ cast,
16
11
  NoReturn,
17
- Optional,
18
- Tuple,
19
- Type,
12
+ NotRequired,
13
+ TextIO,
20
14
  TypeAlias,
15
+ TypedDict,
21
16
  )
17
+ from urllib.parse import urlparse
22
18
 
23
19
  # Third-party imports
24
20
  try:
21
+ from anyio.streams.memory import (
22
+ MemoryObjectReceiveStream,
23
+ MemoryObjectSendStream,
24
+ )
25
25
  from jsonschema_pydantic import jsonschema_to_pydantic # type: ignore
26
26
  from langchain_core.tools import BaseTool, ToolException
27
27
  from mcp import ClientSession
28
+ from mcp.client.sse import sse_client
28
29
  from mcp.client.stdio import stdio_client, StdioServerParameters
30
+ from mcp.client.websocket import websocket_client
29
31
  import mcp.types as mcp_types
30
32
  from pydantic import BaseModel
31
33
  # from pydantic_core import to_json
@@ -35,6 +37,27 @@ except ImportError as e:
35
37
  sys.exit(1)
36
38
 
37
39
 
40
+ class McpServerCommandBasedConfig(TypedDict):
41
+ command: str
42
+ args: NotRequired[list[str] | None]
43
+ env: NotRequired[dict[str, str] | None]
44
+ cwd: NotRequired[str | None]
45
+ errlog: NotRequired[TextIO | None]
46
+
47
+
48
+ class McpServerUrlBasedConfig(TypedDict):
49
+ url: str
50
+ args: NotRequired[list[str] | None]
51
+ env: NotRequired[dict[str, str] | None]
52
+ cwd: NotRequired[str | None]
53
+ errlog: NotRequired[TextIO | None]
54
+
55
+
56
+ McpServerConfig = McpServerCommandBasedConfig | McpServerUrlBasedConfig
57
+
58
+ McpServersConfig = dict[str, McpServerConfig]
59
+
60
+
38
61
  def fix_schema(schema: dict) -> dict:
39
62
  """Converts JSON Schema 'type': ['string', 'null'] to 'anyOf' format"""
40
63
  if isinstance(schema, dict):
@@ -48,7 +71,7 @@ def fix_schema(schema: dict) -> dict:
48
71
 
49
72
  # Type alias for the bidirectional communication channels with the MCP server
50
73
  # FIXME: not defined in mcp.types, really?
51
- StdioTransport: TypeAlias = tuple[
74
+ Transport: TypeAlias = tuple[
52
75
  MemoryObjectReceiveStream[mcp_types.JSONRPCMessage | Exception],
53
76
  MemoryObjectSendStream[mcp_types.JSONRPCMessage]
54
77
  ]
@@ -56,10 +79,10 @@ StdioTransport: TypeAlias = tuple[
56
79
 
57
80
  async def spawn_mcp_server_and_get_transport(
58
81
  server_name: str,
59
- server_config: Dict[str, Any],
82
+ server_config: McpServerConfig,
60
83
  exit_stack: AsyncExitStack,
61
84
  logger: logging.Logger = logging.getLogger(__name__)
62
- ) -> StdioTransport:
85
+ ) -> Transport:
63
86
  """
64
87
  Spawns an MCP server process and establishes communication channels.
65
88
 
@@ -79,54 +102,75 @@ async def spawn_mcp_server_and_get_transport(
79
102
  logger.info(f'MCP server "{server_name}": '
80
103
  f'initializing with: {server_config}')
81
104
 
82
- # NOTE: `uv` and `npx` seem to require PATH to be set.
83
- # To avoid confusion, it was decided to automatically append it
84
- # to the env if not explicitly set by the config.
85
- env = dict(server_config.get('env', {}))
86
- if 'PATH' not in env:
87
- env['PATH'] = os.environ.get('PATH', '')
88
-
89
- # Create server parameters with command, arguments and environment
90
- server_params = StdioServerParameters(
91
- command=server_config['command'],
92
- args=server_config.get('args', []),
93
- env=env,
94
- cwd=server_config.get('cwd', None)
95
- )
105
+ url_str = str(server_config.get('url')) # None becomes 'None'
106
+ # no exception thrown even for a malformed URL
107
+ url_scheme = urlparse(url_str).scheme
96
108
 
97
- # Initialize stdio client and register it with exit stack for cleanup
98
- # NOTE: Why the key name `stderr` for `server_config` was chosen:
99
- # Unlike the TypeScript SDK's `StdioServerParameters`, the Python SDK's
100
- # `StdioServerParameters` doesn't include `stderr`.
101
- # Instead, it calls `stdio_client()` with a separate argument
102
- # `errlog`. I once thought of using `errlog` for the key for the
103
- # Pyhton version, but decided to follow the TypeScript version since
104
- # its public API already exposes the key name and I choose consistency.
105
- stdio_transport = await exit_stack.enter_async_context(
106
- stdio_client(
107
- server_params,
108
- errlog=server_config.get('stderr', None)
109
+ if url_scheme in ('http', 'https'):
110
+ transport = await exit_stack.enter_async_context(
111
+ sse_client(url_str)
112
+ )
113
+
114
+ elif url_scheme in ('ws', 'wss'):
115
+ transport = await exit_stack.enter_async_context(
116
+ websocket_client(url_str)
117
+ )
118
+
119
+ else:
120
+ # NOTE: `uv` and `npx` seem to require PATH to be set.
121
+ # To avoid confusion, it was decided to automatically append it
122
+ # to the env if not explicitly set by the config.
123
+ config = cast(McpServerCommandBasedConfig, server_config)
124
+ # env = config.get('env', {}) does't work since it can yield None
125
+ env_val = config.get('env')
126
+ env = {} if env_val is None else dict(env_val)
127
+ if 'PATH' not in env:
128
+ env['PATH'] = os.environ.get('PATH', '')
129
+
130
+ # Use stdio client for commands
131
+ # args = config.get('args', []) does't work since it can yield None
132
+ args_val = config.get('args')
133
+ args = [] if args_val is None else list(args_val)
134
+ server_parameters = StdioServerParameters(
135
+ command=config.get('command', ''),
136
+ args=args,
137
+ env=env,
138
+ cwd=config.get('cwd', None)
139
+ )
140
+
141
+ # Initialize stdio client and register it with exit stack for
142
+ # cleanup
143
+ # NOTE: Why the key name `errlog` for `server_config` was chosen:
144
+ # Unlike TypeScript SDK's `StdioServerParameters`, the Python
145
+ # SDK's `StdioServerParameters` doesn't include `stderr: int`.
146
+ # Instead, it calls `stdio_client()` with a separate argument
147
+ # `errlog: TextIO`. I once included `stderr: int` for
148
+ # compatibility with the TypeScript version, but decided to
149
+ # follow the Python SDK more closely.
150
+ errlog_val = server_config.get('errlog')
151
+ kwargs = {'errlog': errlog_val} if errlog_val is not None else {}
152
+ transport = await exit_stack.enter_async_context(
153
+ stdio_client(server_parameters, **kwargs)
109
154
  )
110
- )
111
155
  except Exception as e:
112
156
  logger.error(f'Error spawning MCP server: {str(e)}')
113
157
  raise
114
158
 
115
- return stdio_transport
159
+ return transport
116
160
 
117
161
 
118
162
  async def get_mcp_server_tools(
119
163
  server_name: str,
120
- stdio_transport: StdioTransport,
164
+ transport: Transport,
121
165
  exit_stack: AsyncExitStack,
122
166
  logger: logging.Logger = logging.getLogger(__name__)
123
- ) -> List[BaseTool]:
167
+ ) -> list[BaseTool]:
124
168
  """
125
169
  Retrieves and converts MCP server tools to LangChain format.
126
170
 
127
171
  Args:
128
172
  server_name: Server instance name to use for better logging
129
- stdio_transport: Communication channels tuple
173
+ transport: Communication channels tuple
130
174
  exit_stack: Context manager for cleanup handling
131
175
  logger: Logger instance for debugging and monitoring
132
176
 
@@ -137,7 +181,7 @@ async def get_mcp_server_tools(
137
181
  Exception: If tool conversion fails
138
182
  """
139
183
  try:
140
- read, write = stdio_transport
184
+ read, write = transport
141
185
 
142
186
  # Use an intermediate `asynccontextmanager` to log the cleanup message
143
187
  @asynccontextmanager
@@ -164,7 +208,7 @@ async def get_mcp_server_tools(
164
208
  tools_response = await session.list_tools()
165
209
 
166
210
  # Wrap MCP tools into LangChain tools
167
- langchain_tools: List[BaseTool] = []
211
+ langchain_tools: list[BaseTool] = []
168
212
  for tool in tools_response.tools:
169
213
 
170
214
  # Define adapter class to convert MCP tool to LangChain format
@@ -172,10 +216,10 @@ async def get_mcp_server_tools(
172
216
  name: str = tool.name or 'NO NAME'
173
217
  description: str = tool.description or ''
174
218
  # Convert JSON schema to Pydantic model for argument validation
175
- args_schema: Type[BaseModel] = jsonschema_to_pydantic(
219
+ args_schema: type[BaseModel] = jsonschema_to_pydantic(
176
220
  fix_schema(tool.inputSchema) # Apply schema conversion
177
221
  )
178
- session: Optional[ClientSession] = None
222
+ session: ClientSession | None = None
179
223
 
180
224
  def _run(self, **kwargs: Any) -> NoReturn:
181
225
  raise NotImplementedError(
@@ -272,9 +316,9 @@ McpServerCleanupFn = Callable[[], Awaitable[None]]
272
316
 
273
317
 
274
318
  async def convert_mcp_to_langchain_tools(
275
- server_configs: Dict[str, Dict[str, Any]],
276
- logger: Optional[logging.Logger] = None
277
- ) -> Tuple[List[BaseTool], McpServerCleanupFn]:
319
+ server_configs: dict[str, McpServerConfig],
320
+ logger: logging.Logger | None = None
321
+ ) -> tuple[list[BaseTool], McpServerCleanupFn]:
278
322
  """Initialize multiple MCP servers and convert their tools to
279
323
  LangChain format.
280
324
 
@@ -318,7 +362,7 @@ async def convert_mcp_to_langchain_tools(
318
362
  logger = init_logger()
319
363
 
320
364
  # Initialize AsyncExitStack for managing multiple server lifecycles
321
- stdio_transports: List[StdioTransport] = []
365
+ transports: list[Transport] = []
322
366
  async_exit_stack = AsyncExitStack()
323
367
 
324
368
  # Spawn all MCP servers concurrently
@@ -327,24 +371,24 @@ async def convert_mcp_to_langchain_tools(
327
371
  # is spawned, i.e. after returning from the `await`, the spawned
328
372
  # subprocess starts its initialization independently of (so in
329
373
  # parallel with) the Python execution of the following lines.
330
- stdio_transport = await spawn_mcp_server_and_get_transport(
374
+ transport = await spawn_mcp_server_and_get_transport(
331
375
  server_name,
332
376
  server_config,
333
377
  async_exit_stack,
334
378
  logger
335
379
  )
336
- stdio_transports.append(stdio_transport)
380
+ transports.append(transport)
337
381
 
338
382
  # Convert tools from each server to LangChain format
339
- langchain_tools: List[BaseTool] = []
340
- for (server_name, server_config), stdio_transport in zip(
383
+ langchain_tools: list[BaseTool] = []
384
+ for (server_name, server_config), transport in zip(
341
385
  server_configs.items(),
342
- stdio_transports,
386
+ transports,
343
387
  strict=True
344
388
  ):
345
389
  tools = await get_mcp_server_tools(
346
390
  server_name,
347
- stdio_transport,
391
+ transport,
348
392
  async_exit_stack,
349
393
  logger
350
394
  )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: langchain-mcp-tools
3
- Version: 0.1.11
3
+ Version: 0.2.0
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
@@ -12,6 +12,7 @@ Requires-Dist: jsonschema-pydantic>=0.6
12
12
  Requires-Dist: langchain>=0.3.14
13
13
  Requires-Dist: mcp>=1.6.0
14
14
  Requires-Dist: pyjson5>=1.6.8
15
+ Requires-Dist: websockets>=15.0.1
15
16
  Provides-Extra: dev
16
17
  Requires-Dist: dotenv>=0.9.9; extra == "dev"
17
18
  Requires-Dist: langchain-anthropic>=0.3.1; extra == "dev"
@@ -73,13 +74,13 @@ and is expressed as a `dict`, e.g.:
73
74
 
74
75
  ```python
75
76
  mcp_servers = {
76
- 'filesystem': {
77
- 'command': 'npx',
78
- 'args': ['-y', '@modelcontextprotocol/server-filesystem', '.']
77
+ "filesystem": {
78
+ "command": "npx",
79
+ "args": ["-y", "@modelcontextprotocol/server-filesystem", "."]
79
80
  },
80
- 'fetch': {
81
- 'command': 'uvx',
82
- 'args': ['mcp-server-fetch']
81
+ "fetch": {
82
+ "command": "uvx",
83
+ "args": ["mcp-server-fetch"]
83
84
  }
84
85
  }
85
86
 
@@ -90,7 +91,7 @@ tools, cleanup = await convert_mcp_to_langchain_tools(
90
91
 
91
92
  This utility function initializes all specified MCP servers in parallel,
92
93
  and returns LangChain Tools
93
- ([`tools: List[BaseTool]`](https://python.langchain.com/api_reference/core/tools/langchain_core.tools.base.BaseTool.html#langchain_core.tools.base.BaseTool))
94
+ ([`tools: list[BaseTool]`](https://python.langchain.com/api_reference/core/tools/langchain_core.tools.base.BaseTool.html#langchain_core.tools.base.BaseTool))
94
95
  by gathering available MCP tools from the servers,
95
96
  and by wrapping them into LangChain tools.
96
97
  It also returns an async callback function (`cleanup: McpServerCleanupFn`)
@@ -121,13 +122,62 @@ try [this LangChain application built with the utility](https://github.com/hidey
121
122
  For detailed information on how to use this library, please refer to the following document:
122
123
  ["Supercharging LangChain: Integrating 2000+ MCP with ReAct"](https://medium.com/@h1deya/supercharging-langchain-integrating-450-mcp-with-react-d4e467cbf41a)
123
124
 
124
- ## Limitations
125
+ ## Experimental Features
126
+
127
+ ### Remote MCP Server Support
128
+
129
+ `mcp_servers` configuration for SSE and Websocket servers are as follows:
130
+
131
+ ```python
132
+ "sse-server-name": {
133
+ "url": f"http://{sse_server_host}:{sse_server_port}/..."
134
+ },
135
+
136
+ "ws-server-name": {
137
+ "url": f"ws://{ws_server_host}:{ws_server_port}/..."
138
+ },
139
+ ```
140
+
141
+ Note that the key name `"url"` may be changed in the future to match
142
+ the MCP server configurations used by Claude for Desktop once
143
+ it introduces remote server support.
144
+
145
+ ### Working Directory Configuration for Local MCP Servers
125
146
 
126
- Currently, only text results of tool calls are supported.
147
+ The working directory that is used when spawning a local MCP server
148
+ can be specified with the `cwd` key as follows:
127
149
 
128
- Remote MCP server access is not supported.
150
+ ```python
151
+ "local-server-name": {
152
+ "command": "...",
153
+ "args": [...],
154
+ "cwd": "/working/directory" # the working dir to be use by the server
155
+ },
156
+ ```
157
+
158
+ ### Configuration for MCP Server stderr Redirection
159
+
160
+ A new key `errlog` has been introduced in to specify a file-like object
161
+ to which MCP server's stderr is redirected.
162
+
163
+ ```python
164
+ log_path = f"mcp-server-{server_name}.log"
165
+ log_file = open(log_path, "w")
166
+ mcp_servers[server_name]["errlog"] = log_file
167
+ ```
168
+
169
+ **NOTE: Why the key name `errlog` for `server_config` was chosen:**
170
+ Unlike TypeScript SDK's `StdioServerParameters`, the Python
171
+ SDK's `StdioServerParameters` doesn't include `stderr: int`.
172
+ Instead, it calls `stdio_client()` with a separate argument
173
+ `errlog: TextIO`. I once included `stderr: int` for
174
+ compatibility with the TypeScript version, but decided to
175
+ follow the Python SDK more closely.
176
+
177
+ ## Limitations
129
178
 
130
- Optiional MCP client Features, Roots and Sampling, are not supported.
179
+ - Currently, only text results of tool calls are supported.
180
+ - Fatures other than [Tools](https://modelcontextprotocol.io/docs/concepts/tools) are not supported.
131
181
 
132
182
  ## Change Log
133
183
 
@@ -0,0 +1,8 @@
1
+ langchain_mcp_tools/__init__.py,sha256=iatHG2fCpz143wgQUZpyFVilgri4yOh2P0vxr22gguE,114
2
+ langchain_mcp_tools/langchain_mcp_tools.py,sha256=n4u9fN3_sVMG1CmSKk8FrlXrBt79Ly1tz0TLhmDUaic,15118
3
+ langchain_mcp_tools/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
+ langchain_mcp_tools-0.2.0.dist-info/licenses/LICENSE,sha256=CRC91e8v116gCpnp7h49oIa6_zjhxqnHFTREeoZFJwA,1072
5
+ langchain_mcp_tools-0.2.0.dist-info/METADATA,sha256=oCzGJZvkhbhF_OA8Pm-Iy2JSjbeIJEEK1cr8pPxxh1g,6808
6
+ langchain_mcp_tools-0.2.0.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
7
+ langchain_mcp_tools-0.2.0.dist-info/top_level.txt,sha256=aR_9V2A1Yt-Bca60KmndmGLUWb2wiM5IOG-Gkaf1dxY,20
8
+ langchain_mcp_tools-0.2.0.dist-info/RECORD,,
@@ -1,8 +0,0 @@
1
- langchain_mcp_tools/__init__.py,sha256=Xtv2VphhrWB_KlxTIofHZqtCIGtNEl0MxugnrNXTERA,94
2
- langchain_mcp_tools/langchain_mcp_tools.py,sha256=O6TxRRuS-uTOY08-WVKK86Dxx7-dCeFZZRLpBYaPIng,13577
3
- langchain_mcp_tools/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
- langchain_mcp_tools-0.1.11.dist-info/licenses/LICENSE,sha256=CRC91e8v116gCpnp7h49oIa6_zjhxqnHFTREeoZFJwA,1072
5
- langchain_mcp_tools-0.1.11.dist-info/METADATA,sha256=lW4Mm8snEac1hYSuS0szfHmJZFBs79GAEo0bwyEW_sw,5212
6
- langchain_mcp_tools-0.1.11.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
7
- langchain_mcp_tools-0.1.11.dist-info/top_level.txt,sha256=aR_9V2A1Yt-Bca60KmndmGLUWb2wiM5IOG-Gkaf1dxY,20
8
- langchain_mcp_tools-0.1.11.dist-info/RECORD,,