feat: Add MCP integration to enable tools from skills.sh and other MCP servers
- 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
This commit is contained in:
1 parent
55a31278a8
commit
753c7d491e
5 files changed
+380
-11
No files matched your search
+194
@@ -0,0 +1,194 @@
|
||||
"""
|
||||
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
|
||||
Reference in new issue
Block a user