- Added MCP client to discover and convert MCP tools to LangChain Tools - Extended configuration manager to support MCP server configurations - Updated CLI to manage MCP server configurations (list, add, remove, use) - Integrated MCP tools into the agent's tool loading system - Added documentation for MCP integration design
194 lines
6.3 KiB
Python
194 lines
6.3 KiB
Python
"""
|
|
MCP (Model Context Protocol) client for LangCode Agent
|
|
Enables integration with MCP servers like skills.sh to discover and use tools
|
|
"""
|
|
import json
|
|
import requests
|
|
from typing import Dict, List, Any, Optional
|
|
from langchain.tools import Tool
|
|
import logging
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
class MCPClient:
|
|
"""Client for communicating with MCP (Model Context Protocol) servers"""
|
|
|
|
def __init__(self, server_url: str, headers: Optional[Dict[str, str]] = None, timeout: int = 30):
|
|
"""
|
|
Initialize MCP client
|
|
|
|
Args:
|
|
server_url: URL of the MCP server
|
|
headers: Optional headers for authentication
|
|
timeout: Request timeout in seconds
|
|
"""
|
|
self.server_url = server_url.rstrip('/')
|
|
self.headers = headers or {}
|
|
self.timeout = timeout
|
|
self._available_tools: Optional[List[Dict[str, Any]]] = None
|
|
|
|
def _make_request(self, method: str, endpoint: str, data: Optional[Dict] = None) -> Dict[str, Any]:
|
|
"""
|
|
Make a JSON-RPC request to the MCP server
|
|
|
|
Args:
|
|
method: HTTP method (GET, POST, etc.)
|
|
endpoint: API endpoint
|
|
data: Optional data to send
|
|
|
|
Returns:
|
|
Response data as dictionary
|
|
|
|
Raises:
|
|
Exception: If request fails
|
|
"""
|
|
url = f"{self.server_url}{endpoint}"
|
|
|
|
# For MCP, we typically use JSON-RPC over HTTP POST
|
|
if method.upper() == "POST":
|
|
# JSON-RPC 2.0 format
|
|
payload = {
|
|
"jsonrpc": "2.0",
|
|
"method": endpoint.lstrip('/'), # Remove leading slash for method name
|
|
"params": data or {},
|
|
"id": 1
|
|
}
|
|
response = requests.post(
|
|
url,
|
|
json=payload,
|
|
headers={**self.headers, "Content-Type": "application/json"},
|
|
timeout=self.timeout
|
|
)
|
|
else:
|
|
response = requests.request(
|
|
method,
|
|
url,
|
|
headers=self.headers,
|
|
params=data,
|
|
timeout=self.timeout
|
|
)
|
|
|
|
response.raise_for_status()
|
|
|
|
# Handle JSON-RPC response
|
|
if method.upper() == "POST":
|
|
result = response.json()
|
|
if "error" in result:
|
|
raise Exception(f"MCP error: {result['error']}")
|
|
return result.get("result", {})
|
|
else:
|
|
return response.json()
|
|
|
|
def list_tools(self) -> List[Dict[str, Any]]:
|
|
"""
|
|
List available tools from the MCP server
|
|
|
|
Returns:
|
|
List of tool specifications
|
|
"""
|
|
if self._available_tools is not None:
|
|
return self._available_tools
|
|
|
|
try:
|
|
# MCP standard method for listing tools
|
|
result = self._make_request("POST", "/tools/list")
|
|
self._available_tools = result.get("tools", [])
|
|
logger.info(f"Discovered {len(self._available_tools)} tools from MCP server {self.server_url}")
|
|
return self._available_tools
|
|
except Exception as e:
|
|
logger.error(f"Failed to list tools from MCP server {self.server_url}: {e}")
|
|
return []
|
|
|
|
def call_tool(self, tool_name: str, arguments: Dict[str, Any]) -> Any:
|
|
"""
|
|
Call a specific tool on the MCP server
|
|
|
|
Args:
|
|
tool_name: Name of the tool to call
|
|
arguments: Arguments to pass to the tool
|
|
|
|
Returns:
|
|
Tool execution result
|
|
"""
|
|
try:
|
|
result = self._make_request("POST", "/tools/call", {
|
|
"name": tool_name,
|
|
"arguments": arguments
|
|
})
|
|
return result
|
|
except Exception as e:
|
|
logger.error(f"Failed to call tool {tool_name} on MCP server {self.server_url}: {e}")
|
|
raise Exception(f"MCP tool call failed: {e}")
|
|
|
|
def refresh_tools(self):
|
|
"""Refresh the cached list of available tools"""
|
|
self._available_tools = None
|
|
self.list_tools()
|
|
|
|
|
|
def mcp_to_langchain_tool(mcp_client: MCPClient, tool_spec: Dict[str, Any]) -> Tool:
|
|
"""
|
|
Convert an MCP tool specification to a LangChain Tool
|
|
|
|
Args:
|
|
mcp_client: MCP client instance
|
|
tool_spec: Tool specification from MCP server
|
|
|
|
Returns:
|
|
LangChain Tool object
|
|
"""
|
|
def tool_function(*args, **kwargs):
|
|
# Handle both positional and keyword arguments
|
|
if args and len(args) == 1 and isinstance(args[0], dict):
|
|
# Single dict argument passed positionally
|
|
tool_args = args[0]
|
|
elif kwargs:
|
|
# Keyword arguments
|
|
tool_args = kwargs
|
|
else:
|
|
# No arguments or multiple positional args (shouldn't happen for MCP tools)
|
|
tool_args = {}
|
|
|
|
try:
|
|
result = mcp_client.call_tool(tool_spec["name"], tool_args)
|
|
# Convert result to string if it isn't already
|
|
if isinstance(result, str):
|
|
return result
|
|
else:
|
|
return json.dumps(result, indent=2)
|
|
except Exception as e:
|
|
return f"Error calling MCP tool {tool_spec['name']}: {str(e)}"
|
|
|
|
# Extract description, default to empty string if not provided
|
|
description = tool_spec.get("description", "")
|
|
if not description:
|
|
description = f"MCP tool: {tool_spec['name']}"
|
|
|
|
return Tool(
|
|
name=tool_spec["name"],
|
|
description=description,
|
|
func=tool_function
|
|
)
|
|
|
|
|
|
def get_mcp_tools_from_client(mcp_client: MCPClient) -> List[Tool]:
|
|
"""
|
|
Get all available tools from an MCP client as LangChain Tools
|
|
|
|
Args:
|
|
mcp_client: Configured MCP client
|
|
|
|
Returns:
|
|
List of LangChain Tool objects
|
|
"""
|
|
tools = []
|
|
try:
|
|
tool_specs = mcp_client.list_tools()
|
|
for tool_spec in tool_specs:
|
|
langchain_tool = mcp_to_langchain_tool(mcp_client, tool_spec)
|
|
tools.append(langchain_tool)
|
|
logger.info(f"Converted {len(tools)} MCP tools to LangChain Tools")
|
|
except Exception as e:
|
|
logger.error(f"Failed to get tools from MCP client: {e}")
|
|
|
|
return tools |