292 lines
8.0 KiB
TypeScript
292 lines
8.0 KiB
TypeScript
/**
|
|
* MCP Server: MT4 Connector
|
|
*
|
|
* Exposes MT4 trading capabilities as MCP tools for AI agents.
|
|
* Communicates with mt4-gateway service to execute trading operations.
|
|
*
|
|
* @version 0.1.0
|
|
* @author Trading Platform Trading Platform
|
|
*/
|
|
|
|
import express, { Request, Response, NextFunction } from 'express';
|
|
import dotenv from 'dotenv';
|
|
import { mcpToolSchemas, toolHandlers } from './tools';
|
|
import { getMT4Client } from './services/mt4-client';
|
|
|
|
// Load environment variables
|
|
dotenv.config();
|
|
|
|
const app = express();
|
|
const PORT = process.env.PORT || 3605;
|
|
const SERVICE_NAME = 'mcp-mt4-connector';
|
|
const VERSION = '0.1.0';
|
|
|
|
// ==========================================
|
|
// Middleware
|
|
// ==========================================
|
|
|
|
app.use(express.json());
|
|
|
|
// Request logging
|
|
app.use((req: Request, _res: Response, next: NextFunction) => {
|
|
console.log(`[${new Date().toISOString()}] ${req.method} ${req.path}`);
|
|
next();
|
|
});
|
|
|
|
// ==========================================
|
|
// Health & Status Endpoints
|
|
// ==========================================
|
|
|
|
/**
|
|
* Health check endpoint
|
|
*/
|
|
app.get('/health', async (_req: Request, res: Response) => {
|
|
try {
|
|
const client = getMT4Client();
|
|
const mt4Connected = await client.isConnected();
|
|
|
|
res.json({
|
|
status: 'ok',
|
|
service: SERVICE_NAME,
|
|
version: VERSION,
|
|
timestamp: new Date().toISOString(),
|
|
dependencies: {
|
|
mt4Gateway: mt4Connected ? 'connected' : 'disconnected',
|
|
},
|
|
});
|
|
} catch (error) {
|
|
res.json({
|
|
status: 'degraded',
|
|
service: SERVICE_NAME,
|
|
version: VERSION,
|
|
timestamp: new Date().toISOString(),
|
|
dependencies: {
|
|
mt4Gateway: 'error',
|
|
},
|
|
error: error instanceof Error ? error.message : 'Unknown error',
|
|
});
|
|
}
|
|
});
|
|
|
|
/**
|
|
* List available MCP tools
|
|
*/
|
|
app.get('/tools', (_req: Request, res: Response) => {
|
|
res.json({
|
|
tools: mcpToolSchemas,
|
|
count: mcpToolSchemas.length,
|
|
});
|
|
});
|
|
|
|
/**
|
|
* Get specific tool schema
|
|
*/
|
|
app.get('/tools/:toolName', (req: Request, res: Response) => {
|
|
const { toolName } = req.params;
|
|
const tool = mcpToolSchemas.find(t => t.name === toolName);
|
|
|
|
if (!tool) {
|
|
res.status(404).json({
|
|
error: `Tool '${toolName}' not found`,
|
|
availableTools: mcpToolSchemas.map(t => t.name),
|
|
});
|
|
return;
|
|
}
|
|
|
|
res.json(tool);
|
|
});
|
|
|
|
// ==========================================
|
|
// MCP Tool Execution Endpoints
|
|
// ==========================================
|
|
|
|
/**
|
|
* Execute an MCP tool
|
|
* POST /tools/:toolName
|
|
* Body: { parameters: {...} }
|
|
*/
|
|
app.post('/tools/:toolName', async (req: Request, res: Response) => {
|
|
const { toolName } = req.params;
|
|
const { parameters = {} } = req.body;
|
|
|
|
// Validate tool exists
|
|
const handler = toolHandlers[toolName];
|
|
if (!handler) {
|
|
res.status(404).json({
|
|
success: false,
|
|
error: `Tool '${toolName}' not found`,
|
|
availableTools: Object.keys(toolHandlers),
|
|
});
|
|
return;
|
|
}
|
|
|
|
try {
|
|
console.log(`[${new Date().toISOString()}] Executing tool: ${toolName}`);
|
|
console.log(`Parameters: ${JSON.stringify(parameters)}`);
|
|
|
|
const result = await handler(parameters);
|
|
|
|
res.json({
|
|
success: true,
|
|
tool: toolName,
|
|
result,
|
|
});
|
|
} catch (error) {
|
|
console.error(`[${new Date().toISOString()}] Tool error: ${toolName}`, error);
|
|
|
|
// Handle Zod validation errors
|
|
if (error && typeof error === 'object' && 'issues' in error) {
|
|
res.status(400).json({
|
|
success: false,
|
|
error: 'Validation error',
|
|
details: (error as { issues: unknown[] }).issues,
|
|
});
|
|
return;
|
|
}
|
|
|
|
res.status(500).json({
|
|
success: false,
|
|
error: error instanceof Error ? error.message : 'Unknown error',
|
|
});
|
|
}
|
|
});
|
|
|
|
// ==========================================
|
|
// MCP Protocol Endpoints (Standard)
|
|
// ==========================================
|
|
|
|
/**
|
|
* MCP Initialize
|
|
* Returns server capabilities
|
|
*/
|
|
app.post('/mcp/initialize', (_req: Request, res: Response) => {
|
|
res.json({
|
|
protocolVersion: '2024-11-05',
|
|
capabilities: {
|
|
tools: {},
|
|
},
|
|
serverInfo: {
|
|
name: SERVICE_NAME,
|
|
version: VERSION,
|
|
},
|
|
});
|
|
});
|
|
|
|
/**
|
|
* MCP List Tools
|
|
* Returns all available tools in MCP format
|
|
*/
|
|
app.post('/mcp/tools/list', (_req: Request, res: Response) => {
|
|
res.json({
|
|
tools: mcpToolSchemas.map(tool => ({
|
|
name: tool.name,
|
|
description: tool.description,
|
|
inputSchema: tool.inputSchema,
|
|
})),
|
|
});
|
|
});
|
|
|
|
/**
|
|
* MCP Call Tool
|
|
* Execute a tool with parameters
|
|
*/
|
|
app.post('/mcp/tools/call', async (req: Request, res: Response) => {
|
|
const { name, arguments: args = {} } = req.body;
|
|
|
|
if (!name) {
|
|
res.status(400).json({
|
|
error: {
|
|
code: 'invalid_request',
|
|
message: 'Tool name is required',
|
|
},
|
|
});
|
|
return;
|
|
}
|
|
|
|
const handler = toolHandlers[name];
|
|
if (!handler) {
|
|
res.status(404).json({
|
|
error: {
|
|
code: 'unknown_tool',
|
|
message: `Tool '${name}' not found`,
|
|
},
|
|
});
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const result = await handler(args);
|
|
res.json(result);
|
|
} catch (error) {
|
|
// Handle Zod validation errors
|
|
if (error && typeof error === 'object' && 'issues' in error) {
|
|
res.status(400).json({
|
|
error: {
|
|
code: 'invalid_params',
|
|
message: 'Invalid tool parameters',
|
|
data: (error as { issues: unknown[] }).issues,
|
|
},
|
|
});
|
|
return;
|
|
}
|
|
|
|
res.status(500).json({
|
|
error: {
|
|
code: 'internal_error',
|
|
message: error instanceof Error ? error.message : 'Unknown error',
|
|
},
|
|
});
|
|
}
|
|
});
|
|
|
|
// ==========================================
|
|
// Error Handler
|
|
// ==========================================
|
|
|
|
app.use((err: Error, _req: Request, res: Response, _next: NextFunction) => {
|
|
console.error(`[${new Date().toISOString()}] Unhandled error:`, err);
|
|
res.status(500).json({
|
|
error: 'Internal server error',
|
|
message: err.message,
|
|
});
|
|
});
|
|
|
|
// ==========================================
|
|
// Start Server
|
|
// ==========================================
|
|
|
|
app.listen(PORT, () => {
|
|
console.log('');
|
|
console.log('╔══════════════════════════════════════════════════════════╗');
|
|
console.log('║ MCP MT4 Connector - Trading Platform ║');
|
|
console.log('╠══════════════════════════════════════════════════════════╣');
|
|
console.log(`║ Service: ${SERVICE_NAME.padEnd(45)}║`);
|
|
console.log(`║ Version: ${VERSION.padEnd(45)}║`);
|
|
console.log(`║ Port: ${String(PORT).padEnd(48)}║`);
|
|
console.log('╠══════════════════════════════════════════════════════════╣');
|
|
console.log('║ Endpoints: ║');
|
|
console.log(`║ - Health: http://localhost:${PORT}/health`.padEnd(63) + '║');
|
|
console.log(`║ - Tools: http://localhost:${PORT}/tools`.padEnd(63) + '║');
|
|
console.log('╠══════════════════════════════════════════════════════════╣');
|
|
console.log('║ MCP Tools Available: ║');
|
|
mcpToolSchemas.forEach(tool => {
|
|
console.log(`║ - ${tool.name.padEnd(54)}║`);
|
|
});
|
|
console.log('╚══════════════════════════════════════════════════════════╝');
|
|
console.log('');
|
|
});
|
|
|
|
// ==========================================
|
|
// Graceful Shutdown
|
|
// ==========================================
|
|
|
|
process.on('SIGTERM', () => {
|
|
console.log('Received SIGTERM, shutting down gracefully...');
|
|
process.exit(0);
|
|
});
|
|
|
|
process.on('SIGINT', () => {
|
|
console.log('Received SIGINT, shutting down gracefully...');
|
|
process.exit(0);
|
|
});
|