README.mdā¢23.1 kB
# Generic MCP Client Example
This example shows how to create a generic client that can connect to any MCP server, regardless of the AI model being used.
## Overview
This generic client implementation provides:
- HTTP client for MCP servers with HTTP transport
- WebSocket client for real-time communication
- Command-line interface for testing
- REST API wrapper for easy integration
- Language-agnostic approach
## Client Implementations
### Node.js HTTP Client
```javascript
// mcp-client.js
const fetch = require('node-fetch');
const WebSocket = require('ws');
class MCPClient {
constructor(serverUrl, transport = 'http') {
this.serverUrl = serverUrl;
this.transport = transport;
this.ws = null;
}
// HTTP Transport
async callTool(toolName, args) {
if (this.transport === 'http') {
const response = await fetch(`${this.serverUrl}/mcp/tools/${toolName}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ arguments: args }),
});
return await response.json();
}
// WebSocket transport would go here
throw new Error('WebSocket transport not yet implemented');
}
async listTools() {
const response = await fetch(`${this.serverUrl}/mcp/tools`, {
method: 'GET',
});
return await response.json();
}
async getServerInfo() {
const response = await fetch(`${this.serverUrl}/health`);
return await response.json();
}
// WebSocket Transport
connectWebSocket() {
return new Promise((resolve, reject) => {
this.ws = new WebSocket(this.serverUrl.replace('http', 'ws') + '/mcp');
this.ws.on('open', () => {
console.log('WebSocket connected');
resolve();
});
this.ws.on('error', reject);
this.ws.on('message', (data) => {
const message = JSON.parse(data);
this.handleMessage(message);
});
});
}
sendMessage(message) {
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify(message));
}
}
handleMessage(message) {
console.log('Received message:', message);
// Handle incoming messages based on MCP protocol
}
disconnect() {
if (this.ws) {
this.ws.close();
}
}
}
module.exports = MCPClient;
```
### Python HTTP Client
```python
# mcp_client.py
import asyncio
import json
from typing import Dict, Any, List, Optional
import httpx
import websockets
class MCPClient:
def __init__(self, server_url: str, transport: str = 'http'):
self.server_url = server_url
self.transport = transport
self.ws_connection = None
# HTTP Transport
async def call_tool(self, tool_name: str, args: Dict[str, Any]) -> Dict[str, Any]:
"""Call a tool on the MCP server"""
if self.transport == 'http':
async with httpx.AsyncClient() as client:
response = await client.post(
f"{self.server_url}/mcp/tools/{tool_name}",
json={"arguments": args}
)
return response.json()
# WebSocket transport would go here
raise NotImplementedError("WebSocket transport not yet implemented")
async def list_tools(self) -> List[Dict[str, Any]]:
"""List available tools"""
async with httpx.AsyncClient() as client:
response = await client.get(f"{self.server_url}/mcp/tools")
return response.json()
async def get_server_info(self) -> Dict[str, Any]:
"""Get server health and info"""
async with httpx.AsyncClient() as client:
response = await client.get(f"{self.server_url}/health")
return response.json()
# WebSocket Transport
async def connect_websocket(self):
"""Connect to MCP server via WebSocket"""
ws_url = self.server_url.replace('http', 'ws') + '/mcp'
self.ws_connection = await websockets.connect(ws_url)
print(f"WebSocket connected to {ws_url}")
async def send_message(self, message: Dict[str, Any]):
"""Send message via WebSocket"""
if self.ws_connection:
await self.ws_connection.send(json.dumps(message))
async def receive_messages(self):
"""Listen for incoming WebSocket messages"""
if self.ws_connection:
async for message in self.ws_connection:
data = json.loads(message)
await self.handle_message(data)
async def handle_message(self, message: Dict[str, Any]):
"""Handle incoming WebSocket message"""
print(f"Received message: {message}")
# Handle based on MCP protocol
async def disconnect(self):
"""Disconnect WebSocket"""
if self.ws_connection:
await self.ws_connection.close()
# Usage example
async def main():
client = MCPClient('http://localhost:3000')
# Test server connection
info = await client.get_server_info()
print(f"Server info: {info}")
# List available tools
tools = await client.list_tools()
print(f"Available tools: {tools}")
# Call a tool
result = await client.call_tool('get_system_info', {})
print(f"System info: {result}")
if __name__ == "__main__":
asyncio.run(main())
```
### Command Line Interface
```javascript
// cli.js
const MCPClient = require('./mcp-client');
const readline = require('readline');
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
class MCPCLI {
constructor(serverUrl) {
this.client = new MCPClient(serverUrl);
this.tools = [];
}
async init() {
try {
// Test connection
const info = await this.client.getServerInfo();
console.log(`\nš¢ Connected to MCP Server`);
console.log(` Status: ${info.status}`);
console.log(` Timestamp: ${info.timestamp}`);
// Load available tools
this.tools = await this.client.listTools();
console.log(`\nš Available Tools:`);
this.tools.forEach((tool, index) => {
console.log(` ${index + 1}. ${tool.name} - ${tool.description}`);
});
console.log(`\nš” Type 'help' for commands or 'quit' to exit\n`);
this.startPrompt();
} catch (error) {
console.error(`ā Failed to connect: ${error.message}`);
process.exit(1);
}
}
startPrompt() {
rl.question('mcp> ', async (input) => {
await this.handleCommand(input.trim());
this.startPrompt();
});
}
async handleCommand(input) {
const [command, ...args] = input.split(' ');
switch (command.toLowerCase()) {
case 'help':
this.showHelp();
break;
case 'tools':
case 'list':
this.showTools();
break;
case 'call':
await this.callTool(args.join(' '));
break;
case 'info':
await this.showServerInfo();
break;
case 'quit':
case 'exit':
console.log('š Goodbye!');
process.exit(0);
break;
default:
if (input) {
console.log(`ā Unknown command: ${command}. Type 'help' for available commands.`);
}
}
}
showHelp() {
console.log(`
š Available Commands:
`);
console.log(` help - Show this help message`);
console.log(` tools, list - List available tools`);
console.log(` call <tool> - Call a tool (interactive mode)`);
console.log(` info - Show server information`);
console.log(` quit, exit - Exit the CLI`);
console.log(``);
}
showTools() {
console.log(`\nš Available Tools:\n`);
this.tools.forEach((tool, index) => {
console.log(` ${index + 1}. ${tool.name}`);
console.log(` Description: ${tool.description}`);
console.log(` Parameters: ${JSON.stringify(tool.inputSchema?.properties || {}, null, 6)}`);
console.log('');
});
}
async callTool(toolName) {
if (!toolName) {
console.log('š§ Available tools:');
this.tools.forEach((tool, index) => {
console.log(` ${index + 1}. ${tool.name}`);
});
return new Promise((resolve) => {
rl.question('Enter tool name or number: ', async (selection) => {
const tool = isNaN(selection)
? this.tools.find(t => t.name === selection)
: this.tools[parseInt(selection) - 1];
if (tool) {
await this.executeTool(tool);
} else {
console.log('ā Tool not found');
}
resolve();
});
});
}
const tool = this.tools.find(t => t.name === toolName);
if (tool) {
await this.executeTool(tool);
} else {
console.log(`ā Tool '${toolName}' not found`);
}
}
async executeTool(tool) {
console.log(`\nš§ Calling tool: ${tool.name}`);
const params = tool.inputSchema?.properties || {};
const required = tool.inputSchema?.required || [];
const args = {};
// Collect parameters
for (const [paramName, paramSchema] of Object.entries(params)) {
await new Promise((resolve) => {
const isRequired = required.includes(paramName);
const prompt = `${paramName} (${paramSchema.type})${isRequired ? ' *' : ''}: `;
rl.question(prompt, (value) => {
if (value.trim() || !isRequired) {
if (paramSchema.type === 'number') {
args[paramName] = parseFloat(value) || undefined;
} else if (paramSchema.type === 'boolean') {
args[paramName] = value.toLowerCase() === 'true';
} else {
args[paramName] = value || undefined;
}
}
resolve();
});
});
}
try {
console.log('ā³ Executing...');
const result = await this.client.callTool(tool.name, args);
console.log('\nā
Result:');
console.log(JSON.stringify(result, null, 2));
} catch (error) {
console.log(`\nā Error: ${error.message}`);
}
}
async showServerInfo() {
try {
const info = await this.client.getServerInfo();
console.log('\nš„ļø Server Information:');
console.log(JSON.stringify(info, null, 2));
} catch (error) {
console.log(`ā Error getting server info: ${error.message}`);
}
}
}
// Start CLI
const serverUrl = process.argv[2] || 'http://localhost:3000';
const cli = new MCPCLI(serverUrl);
cli.init();
```
## REST API Wrapper
### Express.js Wrapper
```javascript
// rest-wrapper.js
const express = require('express');
const cors = require('cors');
const MCPClient = require('./mcp-client');
class MCPRestWrapper {
constructor(mcpServerUrl, port = 4000) {
this.app = express();
this.client = new MCPClient(mcpServerUrl);
this.port = port;
this.setupMiddleware();
this.setupRoutes();
}
setupMiddleware() {
this.app.use(cors());
this.app.use(express.json());
// Logging middleware
this.app.use((req, res, next) => {
console.log(`${new Date().toISOString()} ${req.method} ${req.path}`);
next();
});
}
setupRoutes() {
// Health check
this.app.get('/health', async (req, res) => {
try {
const serverHealth = await this.client.getServerInfo();
res.json({
wrapper: 'healthy',
mcpServer: serverHealth,
timestamp: new Date().toISOString()
});
} catch (error) {
res.status(500).json({
wrapper: 'healthy',
mcpServer: 'unreachable',
error: error.message
});
}
});
// List tools
this.app.get('/tools', async (req, res) => {
try {
const tools = await this.client.listTools();
res.json(tools);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Call tool
this.app.post('/tools/:toolName', async (req, res) => {
try {
const { toolName } = req.params;
const { arguments: args } = req.body;
const result = await this.client.callTool(toolName, args || {});
res.json(result);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Generic tool calling endpoint
this.app.post('/call', async (req, res) => {
try {
const { tool, arguments: args } = req.body;
if (!tool) {
return res.status(400).json({ error: 'Tool name required' });
}
const result = await this.client.callTool(tool, args || {});
res.json(result);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Batch tool calling
this.app.post('/batch', async (req, res) => {
try {
const { calls } = req.body;
if (!Array.isArray(calls)) {
return res.status(400).json({ error: 'Calls must be an array' });
}
const results = [];
for (const call of calls) {
try {
const result = await this.client.callTool(call.tool, call.arguments || {});
results.push({ success: true, result });
} catch (error) {
results.push({ success: false, error: error.message });
}
}
res.json({ results });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
}
start() {
this.app.listen(this.port, () => {
console.log(`š MCP REST Wrapper running on port ${this.port}`);
console.log(`š Available endpoints:`);
console.log(` GET /health - Health check`);
console.log(` GET /tools - List available tools`);
console.log(` POST /tools/:toolName - Call a specific tool`);
console.log(` POST /call - Generic tool calling`);
console.log(` POST /batch - Batch tool calling`);
});
}
}
// Start wrapper
const mcpServerUrl = process.argv[2] || 'http://localhost:3000';
const wrapperPort = parseInt(process.argv[3]) || 4000;
const wrapper = new MCPRestWrapper(mcpServerUrl, wrapperPort);
wrapper.start();
```
## Usage Examples
### Using the CLI
```powershell
# Start your MCP server
node server.js
# Start the CLI (in another terminal)
node cli.js http://localhost:3000
```
Example CLI session:
```
š¢ Connected to MCP Server
Status: healthy
Timestamp: 2024-01-15T10:30:00.000Z
š Available Tools:
1. read_file - Read the contents of a file
2. write_file - Write content to a file
3. list_directory - List the contents of a directory
4. get_system_info - Get system information
5. execute_command - Execute a system command
6. fetch_url - Fetch content from a URL
š” Type 'help' for commands or 'quit' to exit
mcp> call get_system_info
š§ Calling tool: get_system_info
ā³ Executing...
ā
Result:
{
"content": [
{
"type": "text",
"text": "{\"platform\":\"win32\",\"arch\":\"x64\",\"hostname\":\"DESKTOP-ABC123\",...}"
}
]
}
mcp> call read_file
š§ Calling tool: read_file
path (string) *: package.json
ā³ Executing...
ā
Result:
{
"content": [
{
"type": "text",
"text": "{\n \"name\": \"mcp-implementation\",\n \"version\": \"1.0.0\",\n ..."
}
]
}
```
### Using the REST Wrapper
```powershell
# Start the REST wrapper
node rest-wrapper.js http://localhost:3000 4000
```
Then use HTTP requests:
```bash
# List tools
curl http://localhost:4000/tools
# Call a tool
curl -X POST http://localhost:4000/tools/get_system_info \
-H "Content-Type: application/json" \
-d '{}'
# Generic call
curl -X POST http://localhost:4000/call \
-H "Content-Type: application/json" \
-d '{
"tool": "read_file",
"arguments": {
"path": "README.md"
}
}'
# Batch calls
curl -X POST http://localhost:4000/batch \
-H "Content-Type: application/json" \
-d '{
"calls": [
{
"tool": "get_system_info",
"arguments": {}
},
{
"tool": "list_directory",
"arguments": {
"path": "."
}
}
]
}'
```
## Language Integration Examples
### cURL Examples
```bash
# Health check
curl http://localhost:3000/health
# List directory
curl -X POST http://localhost:3000/tools/list_directory \
-H "Content-Type: application/json" \
-d '{"arguments": {"path": "."}}'
# Read file
curl -X POST http://localhost:3000/tools/read_file \
-H "Content-Type: application/json" \
-d '{"arguments": {"path": "package.json"}}'
```
### PowerShell Examples
```powershell
# Health check
Invoke-RestMethod -Uri "http://localhost:3000/health" -Method GET
# Call tool
$body = @{
arguments = @{
path = "."
}
} | ConvertTo-Json
Invoke-RestMethod -Uri "http://localhost:3000/tools/list_directory" `
-Method POST `
-ContentType "application/json" `
-Body $body
```
### Python Integration
```python
import asyncio
from mcp_client import MCPClient
async def example_usage():
client = MCPClient('http://localhost:3000')
# Get system info
info = await client.call_tool('get_system_info', {})
print(f"System Info: {info}")
# Read a file
file_content = await client.call_tool('read_file', {'path': 'README.md'})
print(f"File Content: {file_content}")
# List directory
directory = await client.call_tool('list_directory', {'path': '.'})
print(f"Directory: {directory}")
if __name__ == "__main__":
asyncio.run(example_usage())
```
## Testing and Debugging
### Health Check Script
```javascript
// health-check.js
const MCPClient = require('./mcp-client');
async function healthCheck(serverUrl) {
const client = new MCPClient(serverUrl);
console.log(`š Testing MCP Server at ${serverUrl}\n`);
try {
// Test server health
console.log('1. Testing server health...');
const health = await client.getServerInfo();
console.log(` ā
Server is ${health.status}\n`);
// Test tool listing
console.log('2. Testing tool listing...');
const tools = await client.listTools();
console.log(` ā
Found ${tools.length} tools\n`);
// Test each tool (with safe parameters)
console.log('3. Testing individual tools...');
// Test get_system_info (no parameters needed)
try {
const sysInfo = await client.callTool('get_system_info', {});
console.log(' ā
get_system_info works');
} catch (e) {
console.log(' ā get_system_info failed:', e.message);
}
// Test list_directory with safe path
try {
const dirList = await client.callTool('list_directory', { path: '.' });
console.log(' ā
list_directory works');
} catch (e) {
console.log(' ā list_directory failed:', e.message);
}
console.log('\nš Health check completed!');
} catch (error) {
console.error(`\nā Health check failed: ${error.message}`);
process.exit(1);
}
}
// Run health check
const serverUrl = process.argv[2] || 'http://localhost:3000';
healthCheck(serverUrl);
```
### Load Testing
```javascript
// load-test.js
const MCPClient = require('./mcp-client');
async function loadTest(serverUrl, concurrency = 10, requests = 100) {
console.log(`š Load testing ${serverUrl}`);
console.log(` Concurrency: ${concurrency}`);
console.log(` Total requests: ${requests}\n`);
const clients = Array(concurrency).fill().map(() => new MCPClient(serverUrl));
const startTime = Date.now();
const results = { success: 0, errors: 0 };
const makeRequest = async (client, requestId) => {
try {
await client.callTool('get_system_info', {});
results.success++;
} catch (error) {
results.errors++;
console.log(` ā Request ${requestId} failed: ${error.message}`);
}
};
const promises = [];
for (let i = 0; i < requests; i++) {
const client = clients[i % concurrency];
promises.push(makeRequest(client, i + 1));
// Add small delay to spread requests
if (i % concurrency === 0) {
await new Promise(resolve => setTimeout(resolve, 10));
}
}
await Promise.all(promises);
const duration = Date.now() - startTime;
const rps = Math.round((requests / duration) * 1000);
console.log(`\nš Load test results:`);
console.log(` Duration: ${duration}ms`);
console.log(` Success: ${results.success}`);
console.log(` Errors: ${results.errors}`);
console.log(` Requests/sec: ${rps}`);
}
// Run load test
const serverUrl = process.argv[2] || 'http://localhost:3000';
const concurrency = parseInt(process.argv[3]) || 10;
const requests = parseInt(process.argv[4]) || 100;
loadTest(serverUrl, concurrency, requests);
```
## Error Handling
### Retry Logic
```javascript
class MCPClientWithRetry extends MCPClient {
async callToolWithRetry(toolName, args, maxRetries = 3, delay = 1000) {
let lastError;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await this.callTool(toolName, args);
} catch (error) {
lastError = error;
if (attempt < maxRetries) {
console.log(`Attempt ${attempt} failed, retrying in ${delay}ms...`);
await new Promise(resolve => setTimeout(resolve, delay));
delay *= 2; // Exponential backoff
}
}
}
throw lastError;
}
}
```
### Circuit Breaker
```javascript
class CircuitBreaker {
constructor(threshold = 5, timeout = 60000) {
this.threshold = threshold;
this.timeout = timeout;
this.failures = 0;
this.nextAttempt = Date.now();
this.state = 'CLOSED'; // CLOSED, OPEN, HALF_OPEN
}
async call(fn) {
if (this.state === 'OPEN') {
if (Date.now() < this.nextAttempt) {
throw new Error('Circuit breaker is OPEN');
}
this.state = 'HALF_OPEN';
}
try {
const result = await fn();
this.onSuccess();
return result;
} catch (error) {
this.onFailure();
throw error;
}
}
onSuccess() {
this.failures = 0;
this.state = 'CLOSED';
}
onFailure() {
this.failures++;
if (this.failures >= this.threshold) {
this.state = 'OPEN';
this.nextAttempt = Date.now() + this.timeout;
}
}
}
```
## Next Steps
1. **Customize for Your Use Case**: Modify the client to match your specific requirements
2. **Add Authentication**: Implement proper authentication and authorization
3. **Monitoring**: Add logging, metrics, and monitoring capabilities
4. **Performance**: Optimize for your expected load and latency requirements
5. **Documentation**: Document your specific tools and usage patterns
This generic client provides a solid foundation for integrating any AI model or application with your MCP server.