Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -488,7 +488,7 @@ server_thread.start()
time.sleep(2) # Allow server to start

# Convert MCP tool to LangChain
calculator_tool = to_langchain_tool("http://localhost:5000", "calculator")
calculator_tool = asyncio.run(to_langchain_tool("http://localhost:5000", "calculator"))

# Use the tool in LangChain
result = calculator_tool.run("5 * 9 + 3")
Expand Down
2 changes: 1 addition & 1 deletion README_de.md
Original file line number Diff line number Diff line change
Expand Up @@ -473,7 +473,7 @@ server_thread.start()
time.sleep(2) # Allow server to start

# Convert MCP tool to LangChain
calculator_tool = to_langchain_tool("http://localhost:5000", "calculator")
calculator_tool = asyncio.run(to_langchain_tool("http://localhost:5000", "calculator"))

# Use the tool in LangChain
result = calculator_tool.run("5 * 9 + 3")
Expand Down
2 changes: 1 addition & 1 deletion README_es.md
Original file line number Diff line number Diff line change
Expand Up @@ -473,7 +473,7 @@ server_thread.start()
time.sleep(2) # Allow server to start

# Convert MCP tool to LangChain
calculator_tool = to_langchain_tool("http://localhost:5000", "calculator")
calculator_tool = asyncio.run(to_langchain_tool("http://localhost:5000", "calculator"))

# Use the tool in LangChain
result = calculator_tool.run("5 * 9 + 3")
Expand Down
2 changes: 1 addition & 1 deletion README_fr.md
Original file line number Diff line number Diff line change
Expand Up @@ -473,7 +473,7 @@ server_thread.start()
time.sleep(2) # Permettre au serveur de démarrer

# Convertir l'outil MCP vers LangChain
calculator_tool = to_langchain_tool("http://localhost:5000", "calculator")
calculator_tool = asyncio.run(to_langchain_tool("http://localhost:5000", "calculator"))

# Utiliser l'outil dans LangChain
result = calculator_tool.run("5 * 9 + 3")
Expand Down
2 changes: 1 addition & 1 deletion README_ja.md
Original file line number Diff line number Diff line change
Expand Up @@ -474,7 +474,7 @@ server_thread.start()
time.sleep(2) # サーバーの起動を待つ

# MCP ツールを LangChain に変換
calculator_tool = to_langchain_tool("http://localhost:5000", "calculator")
calculator_tool = asyncio.run(to_langchain_tool("http://localhost:5000", "calculator"))

# LangChain でツールを使用
result = calculator_tool.run("5 * 9 + 3")
Expand Down
2 changes: 1 addition & 1 deletion README_zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -473,7 +473,7 @@ server_thread.start()
time.sleep(2) # 允许服务器启动

# 将 MCP 工具转换为 LangChain
calculator_tool = to_langchain_tool("http://localhost:5000", "calculator")
calculator_tool = asyncio.run(to_langchain_tool("http://localhost:5000", "calculator"))

# 在 LangChain 中使用工具
result = calculator_tool.run("5 * 9 + 3")
Expand Down
3 changes: 2 additions & 1 deletion examples/langchain/mcp_to_langchain.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import time
import threading
import socket
import asyncio

def find_available_port(start_port=5000, max_tries=10):
"""Find an available port"""
Expand Down Expand Up @@ -91,7 +92,7 @@ def calculator(input):

# 3. Convert the MCP tool to a LangChain tool
print(f"\nConverting MCP tool to LangChain tool from {server_url}")
calculator_tool = to_langchain_tool(server_url, "calculator")
calculator_tool = asyncio.run(to_langchain_tool(server_url, "calculator"))

# 4. Use the LangChain tool
print("\nUsing the LangChain tool:")
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ dev = [
"flake8>=3.9.2",
"mypy>=0.812",
"responses>=0.13.3",
"mcp>=1.11.0"
]
server = [
"flask>=2.0.0",
Expand Down
6 changes: 3 additions & 3 deletions python_a2a/langchain/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,10 +143,10 @@ server.run(port=8080)
from python_a2a.langchain import to_langchain_tool

# Convert all tools from an MCP server
tools = to_langchain_tool("http://localhost:8080")
tools = asyncio.run(to_langchain_tool("http://localhost:8080"))

# Convert a specific tool
calculator_tool = to_langchain_tool("http://localhost:8080", "calculator")
calculator_tool = asyncio.run(to_langchain_tool("http://localhost:8080", "calculator"))

# Use in LangChain
from langchain.agents import initialize_agent
Expand Down Expand Up @@ -191,7 +191,7 @@ The integration provides detailed error handling with specific exception types:

```python
try:
tool = to_langchain_tool("http://localhost:8080", "non_existent_tool")
tool = asyncio.run(to_langchain_tool("http://localhost:8080", "non_existent_tool"))
except MCPToolConversionError as e:
print(f"MCP tool error: {e}")
```
Expand Down
6 changes: 6 additions & 0 deletions python_a2a/langchain/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ def __init__(self, message=None):
self.message = message or "LangChain is not installed. Install it with 'pip install langchain langchain_core'"
super().__init__(self.message)

class MCPNotInstalledError(Exception):
"""Raised when mcp is not installed"""

def __init__(self, message=None):
self.message = message or "mcp is not installed. Install it with 'pip install mcp'"

class LangChainToolConversionError(LangChainIntegrationError):
"""Raised when a LangChain tool cannot be converted."""
pass
Expand Down
214 changes: 91 additions & 123 deletions python_a2a/langchain/mcp.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,23 @@
import json
import requests
from typing import Any, Dict, List, Optional, Union, Callable, Type, get_type_hints
import traceback

logger = logging.getLogger(__name__)

# Import custom exceptions
from .exceptions import (
LangChainNotInstalledError,
LangChainToolConversionError,
MCPToolConversionError
MCPToolConversionError,
MCPNotInstalledError
)

# Check for LangChain availability without failing
try:
# Try to import LangChain components
try:
from langchain_core.tools import BaseTool, ToolException
from langchain_core.tools import BaseTool, ToolException, tool
except ImportError:
# Fall back to older LangChain structure
from langchain.tools import BaseTool, ToolException
Expand All @@ -40,6 +42,13 @@ class BaseTool:
class ToolException(Exception):
pass

# Check for MCP availability without failing
try:
from mcp import ClientSession, Tool as MCPTool
from mcp.client.streamable_http import streamablehttp_client
HAS_MCP = True
except ImportError:
HAS_MCP = False

# Utility for mapping between Python types and MCP types
class TypeMapper:
Expand Down Expand Up @@ -448,11 +457,61 @@ async def wrapper(**kwargs):
except Exception as e:
logger.exception("Failed to create MCP server from LangChain tools")
raise LangChainToolConversionError(f"Failed to convert LangChain tools: {str(e)}")



async def get_tool(available_tool: MCPTool, mcp_url: str):
"""
Get langchain tool function for a mentioned MCP Tool from MCP Server
"""
tool_name = available_tool.name
if not tool_name:
logger.exception("Tool Name not found in the tool")
raise LangChainToolConversionError("Tool Name not found in the tool")
parameters = available_tool.inputSchema["properties"].values()
args = [i["title"].lower() for i in parameters]
args = [i.replace(" ", "_") for i in args]
arg_names = ", ".join(args)
func_def_str = f"""
@tool
async def {tool_name}({arg_names}):
'''Call MCP tool function'''
args2 = "{arg_names}".split(", ")
body = {{}}
for a in args2:
body[a] = locals()[a]
try:
async with streamablehttp_client("{mcp_url}") as (
read_stream, write_stream, _,
):
async with ClientSession(read_stream, write_stream) as session:
await session.initialize()
result = await session.call_tool("{tool_name}", body)
if "error" in result:
return f"Error: {{result['error']}}"

# Process content in response
if "content" in result:
content = result.get("content", [])
if content and isinstance(content, list) and "text" in content[0]:
return content[0]["text"]

# If no structured content, return the raw result
return str(result)

except BaseException as e:
return f"Error while calling tool: {{e}}"
"""
dynamic_scope = {}

def to_langchain_tool(mcp_url, tool_name=None):
exec(func_def_str, globals(), dynamic_scope)
the_tool = dynamic_scope[tool_name]
return the_tool


async def to_langchain_tool(mcp_url, tool_name=None):
"""
Convert MCP server tool(s) to LangChain tool(s).
Get Convert MCP server tool(s) & Convert them to LangChain tool(s).

Args:
mcp_url: URL of the MCP server
Expand All @@ -463,10 +522,13 @@ def to_langchain_tool(mcp_url, tool_name=None):

Example:
>>> # Convert a specific tool
>>> calculator_tool = to_langchain_tool("http://localhost:8000", "calculator")
>>>
>>> calculator_tool = await to_langchain_tool("http://localhost:8080/mcp", "calculator")
>>> #or
>>> calculator_tool = asyncio.run(to_langchain_tool("http://localhost:8080/mcp", "calculator"))
>>> # Convert all tools from a server
>>> tools = to_langchain_tool("http://localhost:8000")
>>> tools = await to_langchain_tool("http://localhost:8080/mcp")
>>> #or
>>> tools = asyncio.run(to_langchain_tool("http://localhost:8080/mcp"))

Raises:
LangChainNotInstalledError: If LangChain is not installed
Expand All @@ -475,133 +537,39 @@ def to_langchain_tool(mcp_url, tool_name=None):
if not HAS_LANGCHAIN:
raise LangChainNotInstalledError()

try:
# Try to import Tool from various possible locations in LangChain
try:
from langchain.tools import Tool
except ImportError:
try:
from langchain.agents import Tool
except ImportError:
raise ImportError("Cannot import Tool class from LangChain")
if not HAS_MCP:
raise MCPNotInstalledError()

try:
# Get available tools from MCP server
langchain_tools = []
try:
tools_response = requests.get(f"{mcp_url}/tools")
if tools_response.status_code != 200:
raise MCPToolConversionError(f"Failed to get tools from MCP server: {tools_response.status_code}")

available_tools = tools_response.json()
logger.info(f"Found {len(available_tools)} tools on MCP server")
async with streamablehttp_client(f"{mcp_url}") as (
read_stream, write_stream, _,
):
async with ClientSession(read_stream, write_stream) as session:
await session.initialize()
available_tools = await session.list_tools()

if tool_name:
available_tool = [t for t in available_tools.tools if t.name == tool_name][0]
the_tool = await get_tool(available_tool, mcp_url)
return the_tool

all_available_tools = [t for t in available_tools.tools ]
for a_tool in all_available_tools:
the_tool = await get_tool(a_tool, mcp_url)
langchain_tools.append(the_tool)
return langchain_tools
except Exception as e:
logger.error(f"Error getting tools from MCP server: {e}")
raise MCPToolConversionError(f"Failed to get tools from MCP server: {str(e)}")

# Filter tools if a specific tool is requested
if tool_name is not None:
available_tools = [t for t in available_tools if t.get("name") == tool_name]
if not available_tools:
raise MCPToolConversionError(f"Tool '{tool_name}' not found on MCP server at {mcp_url}")

# Create LangChain tools
langchain_tools = []

for tool_info in available_tools:
name = tool_info.get("name", "unnamed_tool")
description = tool_info.get("description", f"MCP Tool: {name}")
parameters = tool_info.get("parameters", [])

logger.info(f"Creating LangChain tool for MCP tool: {name}")

# Create function to call the MCP tool
def create_tool_func(tool_name):
# Need this wrapper to properly capture tool_name in closure
def tool_func(*args, **kwargs):
"""Call MCP tool function"""
try:
# Handle different input patterns
if len(args) == 1 and not kwargs:
input_value = args[0]

# If the input looks like a parameter=value string (for multi-param tools)
if '=' in input_value and not input_value.startswith('{'):
# Parse simple param=value&param2=value2 format
params = {}
for pair in input_value.split('&'):
if '=' in pair:
k, v = pair.split('=', 1)
params[k.strip()] = v.strip()
if params:
kwargs = params
# Try to detect parameter format based on tool parameters
elif parameters and len(parameters) == 1:
# Single parameter case - use the parameter name from tool info
param_name = parameters[0]["name"]
kwargs = {param_name: input_value}
else:
# Default to 'input' parameter
kwargs = {"input": input_value}

# Call the MCP tool
response = requests.post(
f"{mcp_url}/tools/{tool_name}",
json=kwargs
)

if response.status_code != 200:
return f"Error: HTTP {response.status_code} - {response.text}"

# Parse the response
result = response.json()

# Process error in response
if "error" in result:
return f"Error: {result['error']}"

# Process content in response
if "content" in result:
content = result.get("content", [])
if content and isinstance(content, list) and "text" in content[0]:
return content[0]["text"]

# If no structured content, return the raw result
return str(result)
except Exception as e:
logger.exception(f"Error calling MCP tool {tool_name}")
return f"Error calling tool: {str(e)}"

return tool_func

# Create the tool with a function that properly handles tool name in closure
tool_func = create_tool_func(name)

# Create the LangChain tool
lc_tool = Tool(
name=name,
description=description,
func=tool_func
)

# Add metadata if applicable
if hasattr(lc_tool, "metadata"):
lc_tool.metadata = {
"source": "mcp",
"url": mcp_url,
"parameters": parameters
}

langchain_tools.append(lc_tool)
logger.info(f"Successfully created LangChain tool: {name}")

# Return single tool if requested, otherwise return list
if tool_name is not None and len(langchain_tools) == 1:
return langchain_tools[0]

return langchain_tools

except MCPToolConversionError:
# Re-raise without wrapping
raise
except Exception as e:
logger.exception("Failed to convert MCP tool to LangChain format")
raise MCPToolConversionError(f"Failed to convert MCP tool: {str(e)}")
raise MCPToolConversionError(f"Failed to convert MCP tool: {str(e)}")
Loading