Files

195 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_core.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}")
self._available_tools = []
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