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.
@@ -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'\nError: Required package not found: {e}')
36
- print('Please ensure all required packages are installed\n')
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 'type': ['string', 'null'] to 'anyOf' format"""
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 'type' in schema and isinstance(schema['type'], list):
62
- schema['anyOf'] = [{'type': t} for t in schema['type']]
63
- del schema['type'] # Remove 'type' and standardize to 'anyOf'
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'initializing with: {server_config}')
106
+ f"initializing with: {server_config}")
101
107
 
102
- url_str = str(server_config.get('url')) # None becomes 'None'
103
- headers = server_config.get("headers", None)
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 ('http', 'https'):
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 ('ws', 'wss'):
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('env', {}) does't work since it can yield None
123
- env_val = config.get('env')
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 'PATH' not in env:
126
- env['PATH'] = os.environ.get('PATH', '')
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('args', []) does't work since it can yield None
130
- args_val = config.get('args')
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('command', ''),
140
+ command=config.get("command", ""),
134
141
  args=args,
135
142
  env=env,
136
- cwd=config.get('cwd', None)
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.get('errlog')
149
- kwargs = {'errlog': errlog_val} if errlog_val is not None else {}
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'Error spawning MCP server: {str(e)}')
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 'NO NAME'
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
- 'MCP tools only support async operations'
231
+ "MCP tools only support async operations"
225
232
  )
226
233
 
227
234
  async def _arun(self, **kwargs: Any) -> Any:
228
- """
229
- Asynchronously executes the tool with given arguments.
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'received input: {kwargs}')
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, 'isError') and result.isError:
254
+ if hasattr(result, "isError") and result.isError:
239
255
  raise ToolException(
240
- f'Tool execution failed: {result.content}'
256
+ f"Tool execution failed: {result.content}"
241
257
  )
242
258
 
243
- if not hasattr(result, 'content'):
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 = '\n\n'.join(
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'Error in parsing result.content: {str(e)}; '
263
- f'contents: {repr(result.content)}'
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'received result (size: {size})')
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
- 'No text content available in response'
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'caused error: {str(e)}'
299
+ f"caused error: {str(e)}"
284
300
  )
285
301
  if self.handle_tool_error:
286
- return f'Error executing MCP tool: {str(e)}'
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'tool(s) available:')
309
+ f"tool(s) available:")
294
310
  for tool in langchain_tools:
295
- logger.info(f'- {tool.name}')
311
+ logger.info(f"- {tool.name}")
296
312
  except Exception as e:
297
- logger.error(f'Error getting MCP tools: {str(e)}')
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='\x1b[90m[%(levelname)s]\x1b[0m %(message)s'
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
- If None, uses module logger with fallback to a pre-configured
333
- logger when no root handlers exist.
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
- - List of converted LangChain tools from all servers
338
- - Async cleanup function to properly shutdown all server
339
- connections
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
- 'fetch': {
344
- 'command': 'uvx', 'args': ['mcp-server-fetch']
365
+ "fetch": {
366
+ "command": "uvx", "args": ["mcp-server-fetch"]
345
367
  },
346
- 'weather': {
347
- 'command': 'npx', 'args': ['-y','@h1deya/mcp-server-weather']
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'MCP servers initialized: {len(langchain_tools)} tool(s) '
402
- f'available in total')
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'- {tool.name}')
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.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 [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://github.com/hideya/langchain-mcp-tools-py/blob/main/LICENSE) [![pypi version](https://img.shields.io/pypi/v/langchain-mcp-tools.svg)](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/cf96ddc43750708ef3b244bad95714f0f2fe1d28/src/example.py#L43-L54)
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
- ### Passing HTTP Headers to SSE Connection
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
- ### Configuration for Local MCP Server `stderr` Redirection
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,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (78.1.0)
2
+ Generator: setuptools (79.0.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -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,,