299 lines
10 KiB
TypeScript
299 lines
10 KiB
TypeScript
/**
|
|
* MCP Investment Server
|
|
* Agent allocations and profit distribution
|
|
*/
|
|
|
|
import 'dotenv/config';
|
|
import express, { Request, Response, NextFunction } from 'express';
|
|
import helmet from 'helmet';
|
|
import cors from 'cors';
|
|
import { serverConfig, closePool } from './config';
|
|
import { logger } from './utils/logger';
|
|
import { allToolSchemas, allToolHandlers, listTools } from './tools';
|
|
import { authMiddleware, adminMiddleware } from './middleware';
|
|
|
|
const app = express();
|
|
|
|
// Middleware
|
|
app.use(helmet());
|
|
app.use(cors());
|
|
app.use(express.json({ limit: '1mb' }));
|
|
|
|
// Request logging
|
|
app.use((req: Request, _res: Response, next: NextFunction) => {
|
|
logger.debug('Incoming request', {
|
|
method: req.method,
|
|
path: req.path,
|
|
});
|
|
next();
|
|
});
|
|
|
|
// Health check
|
|
app.get('/health', (_req: Request, res: Response) => {
|
|
res.json({ status: 'healthy', service: 'mcp-investment', timestamp: new Date().toISOString() });
|
|
});
|
|
|
|
// MCP Endpoints
|
|
|
|
// List available tools
|
|
app.get('/mcp/tools', (_req: Request, res: Response) => {
|
|
res.json({ tools: listTools() });
|
|
});
|
|
|
|
// Execute tool
|
|
app.post('/mcp/tools/:toolName', async (req: Request, res: Response) => {
|
|
const { toolName } = req.params;
|
|
const handler = allToolHandlers[toolName];
|
|
|
|
if (!handler) {
|
|
res.status(404).json({ success: false, error: `Tool '${toolName}' not found`, code: 'TOOL_NOT_FOUND' });
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const result = await handler(req.body);
|
|
res.json(result);
|
|
} catch (error) {
|
|
logger.error('Tool execution error', { toolName, error });
|
|
res.status(500).json({ success: false, error: 'Internal server error', code: 'INTERNAL_ERROR' });
|
|
}
|
|
});
|
|
|
|
// ============================================================================
|
|
// REST API Endpoints - Protected by Auth
|
|
// ============================================================================
|
|
|
|
// Agent configs (public info, but tenant-scoped)
|
|
const agentsRouter = express.Router();
|
|
agentsRouter.use(authMiddleware);
|
|
|
|
agentsRouter.get('/', async (req: Request, res: Response) => {
|
|
const result = await allToolHandlers.investment_list_agents({ tenantId: req.tenantId });
|
|
res.json(result);
|
|
});
|
|
|
|
agentsRouter.get('/:agentType', async (req: Request, res: Response) => {
|
|
const result = await allToolHandlers.investment_get_agent({
|
|
tenantId: req.tenantId,
|
|
agentType: req.params.agentType,
|
|
});
|
|
res.json(result);
|
|
});
|
|
|
|
agentsRouter.get('/:agentType/performance', async (req: Request, res: Response) => {
|
|
const result = await allToolHandlers.investment_get_agent_performance({
|
|
tenantId: req.tenantId,
|
|
agentType: req.params.agentType,
|
|
periodDays: req.query.periodDays ? parseInt(req.query.periodDays as string, 10) : undefined,
|
|
});
|
|
res.json(result);
|
|
});
|
|
|
|
agentsRouter.get('/:agentType/distributions', async (req: Request, res: Response) => {
|
|
const result = await allToolHandlers.investment_get_distributions({
|
|
tenantId: req.tenantId,
|
|
agentType: req.params.agentType,
|
|
limit: req.query.limit ? parseInt(req.query.limit as string, 10) : undefined,
|
|
});
|
|
res.json(result);
|
|
});
|
|
|
|
app.use('/api/v1/agents', agentsRouter);
|
|
|
|
// Allocations - Protected
|
|
const allocationsRouter = express.Router();
|
|
allocationsRouter.use(authMiddleware);
|
|
|
|
// Create allocation
|
|
allocationsRouter.post('/', async (req: Request, res: Response) => {
|
|
const result = await allToolHandlers.investment_create_allocation({
|
|
...req.body,
|
|
tenantId: req.tenantId,
|
|
userId: req.userId,
|
|
});
|
|
res.status(result.success ? 201 : 400).json(result);
|
|
});
|
|
|
|
// List allocations
|
|
allocationsRouter.get('/', async (req: Request, res: Response) => {
|
|
const result = await allToolHandlers.investment_list_allocations({
|
|
tenantId: req.tenantId,
|
|
userId: req.query.userId || req.userId, // Default to current user
|
|
agentType: req.query.agentType,
|
|
status: req.query.status,
|
|
limit: req.query.limit ? parseInt(req.query.limit as string, 10) : undefined,
|
|
offset: req.query.offset ? parseInt(req.query.offset as string, 10) : undefined,
|
|
});
|
|
res.json(result);
|
|
});
|
|
|
|
// Get allocation
|
|
allocationsRouter.get('/:allocationId', async (req: Request, res: Response) => {
|
|
const result = await allToolHandlers.investment_get_allocation({
|
|
tenantId: req.tenantId,
|
|
allocationId: req.params.allocationId,
|
|
});
|
|
res.json(result);
|
|
});
|
|
|
|
// Fund allocation
|
|
allocationsRouter.post('/:allocationId/fund', async (req: Request, res: Response) => {
|
|
const result = await allToolHandlers.investment_fund_allocation({
|
|
...req.body,
|
|
tenantId: req.tenantId,
|
|
allocationId: req.params.allocationId,
|
|
userId: req.userId,
|
|
});
|
|
res.json(result);
|
|
});
|
|
|
|
// Withdraw from allocation
|
|
allocationsRouter.post('/:allocationId/withdraw', async (req: Request, res: Response) => {
|
|
const result = await allToolHandlers.investment_withdraw({
|
|
...req.body,
|
|
tenantId: req.tenantId,
|
|
allocationId: req.params.allocationId,
|
|
userId: req.userId,
|
|
});
|
|
res.json(result);
|
|
});
|
|
|
|
// Update allocation status
|
|
allocationsRouter.patch('/:allocationId/status', async (req: Request, res: Response) => {
|
|
const result = await allToolHandlers.investment_update_status({
|
|
...req.body,
|
|
tenantId: req.tenantId,
|
|
allocationId: req.params.allocationId,
|
|
});
|
|
res.json(result);
|
|
});
|
|
|
|
// Get allocation transactions
|
|
allocationsRouter.get('/:allocationId/transactions', async (req: Request, res: Response) => {
|
|
const result = await allToolHandlers.investment_get_transactions({
|
|
tenantId: req.tenantId,
|
|
allocationId: req.params.allocationId,
|
|
limit: req.query.limit ? parseInt(req.query.limit as string, 10) : undefined,
|
|
offset: req.query.offset ? parseInt(req.query.offset as string, 10) : undefined,
|
|
});
|
|
res.json(result);
|
|
});
|
|
|
|
app.use('/api/v1/allocations', allocationsRouter);
|
|
|
|
// User endpoints - Protected
|
|
const userRouter = express.Router();
|
|
userRouter.use(authMiddleware);
|
|
|
|
// Get current user's allocations
|
|
userRouter.get('/me/allocations', async (req: Request, res: Response) => {
|
|
const result = await allToolHandlers.investment_get_user_allocations({
|
|
tenantId: req.tenantId,
|
|
userId: req.userId,
|
|
});
|
|
res.json(result);
|
|
});
|
|
|
|
// Get current user's investment summary
|
|
userRouter.get('/me/investment-summary', async (req: Request, res: Response) => {
|
|
const result = await allToolHandlers.investment_get_user_summary({
|
|
tenantId: req.tenantId,
|
|
userId: req.userId,
|
|
});
|
|
res.json(result);
|
|
});
|
|
|
|
// Get specific user's allocations (for admins or own data)
|
|
userRouter.get('/:userId/allocations', async (req: Request, res: Response) => {
|
|
// Users can only see their own allocations unless they're owners
|
|
if (req.params.userId !== req.userId && !req.isOwner) {
|
|
res.status(403).json({ success: false, error: 'Forbidden', code: 'ACCESS_DENIED' });
|
|
return;
|
|
}
|
|
const result = await allToolHandlers.investment_get_user_allocations({
|
|
tenantId: req.tenantId,
|
|
userId: req.params.userId,
|
|
});
|
|
res.json(result);
|
|
});
|
|
|
|
// Get specific user's investment summary
|
|
userRouter.get('/:userId/investment-summary', async (req: Request, res: Response) => {
|
|
if (req.params.userId !== req.userId && !req.isOwner) {
|
|
res.status(403).json({ success: false, error: 'Forbidden', code: 'ACCESS_DENIED' });
|
|
return;
|
|
}
|
|
const result = await allToolHandlers.investment_get_user_summary({
|
|
tenantId: req.tenantId,
|
|
userId: req.params.userId,
|
|
});
|
|
res.json(result);
|
|
});
|
|
|
|
app.use('/api/v1/users', userRouter);
|
|
|
|
// Admin endpoints - Protected + Admin only
|
|
const adminRouter = express.Router();
|
|
adminRouter.use(authMiddleware);
|
|
adminRouter.use(adminMiddleware);
|
|
|
|
// Distribute profits (admin only)
|
|
adminRouter.post('/distribute-profits', async (req: Request, res: Response) => {
|
|
const result = await allToolHandlers.investment_distribute_profits({
|
|
...req.body,
|
|
tenantId: req.tenantId,
|
|
distributedBy: req.userId,
|
|
});
|
|
res.json(result);
|
|
});
|
|
|
|
app.use('/api/v1/admin', adminRouter);
|
|
|
|
// ============================================================================
|
|
// Error Handling
|
|
// ============================================================================
|
|
|
|
app.use((_req: Request, res: Response) => {
|
|
res.status(404).json({ success: false, error: 'Not found' });
|
|
});
|
|
|
|
app.use((err: Error, _req: Request, res: Response, _next: NextFunction) => {
|
|
logger.error('Unhandled error', { error: err.message, stack: err.stack });
|
|
res.status(500).json({ success: false, error: 'Internal server error' });
|
|
});
|
|
|
|
// ============================================================================
|
|
// Server Startup
|
|
// ============================================================================
|
|
|
|
async function shutdown() {
|
|
logger.info('Shutting down...');
|
|
await closePool();
|
|
process.exit(0);
|
|
}
|
|
|
|
process.on('SIGTERM', shutdown);
|
|
process.on('SIGINT', shutdown);
|
|
|
|
app.listen(serverConfig.port, () => {
|
|
logger.info(`MCP Investment Server running on port ${serverConfig.port}`, {
|
|
env: serverConfig.env,
|
|
tools: Object.keys(allToolSchemas).length,
|
|
});
|
|
console.log(`
|
|
╔════════════════════════════════════════════════════════════╗
|
|
║ MCP INVESTMENT SERVER ║
|
|
╠════════════════════════════════════════════════════════════╣
|
|
║ Port: ${serverConfig.port} ║
|
|
║ Tools: ${String(Object.keys(allToolSchemas).length).padEnd(12)} ║
|
|
╠════════════════════════════════════════════════════════════╣
|
|
║ /api/v1/agents/* - Agent info ║
|
|
║ /api/v1/allocations/* - Investment allocations ║
|
|
║ /api/v1/users/* - User investments ║
|
|
║ /api/v1/admin/* - Admin operations ║
|
|
╚════════════════════════════════════════════════════════════╝
|
|
`);
|
|
});
|
|
|
|
export default app;
|