mcp-twin 1.2.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,186 @@
1
+ /**
2
+ * Usage Routes
3
+ * MCP Twin Cloud
4
+ */
5
+
6
+ import { Router, Request, Response } from 'express';
7
+ import { query, queryOne } from '../db';
8
+ import { authenticateApiKey, getTierLimits } from '../auth';
9
+
10
+ const router = Router();
11
+
12
+ interface UsageDaily {
13
+ id: string;
14
+ user_id: string;
15
+ date: string;
16
+ request_count: number;
17
+ error_count: number;
18
+ total_duration_ms: number;
19
+ }
20
+
21
+ /**
22
+ * GET /api/usage
23
+ * Get current month usage
24
+ */
25
+ router.get('/', authenticateApiKey, async (req: Request, res: Response) => {
26
+ try {
27
+ const user = req.user!;
28
+ const limits = getTierLimits(user.tier);
29
+
30
+ // Get current month start
31
+ const now = new Date();
32
+ const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
33
+ const endOfMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0);
34
+
35
+ // Get aggregated usage for current month
36
+ const usage = await queryOne<{
37
+ total_requests: string;
38
+ total_errors: string;
39
+ total_duration: string;
40
+ }>(
41
+ `SELECT
42
+ COALESCE(SUM(request_count), 0) as total_requests,
43
+ COALESCE(SUM(error_count), 0) as total_errors,
44
+ COALESCE(SUM(total_duration_ms), 0) as total_duration
45
+ FROM usage_daily
46
+ WHERE user_id = $1 AND date >= $2 AND date <= $3`,
47
+ [user.id, startOfMonth.toISOString().split('T')[0], endOfMonth.toISOString().split('T')[0]]
48
+ );
49
+
50
+ // Get twin count
51
+ const twinCount = await queryOne<{ count: string }>(
52
+ 'SELECT COUNT(*) as count FROM twins WHERE user_id = $1',
53
+ [user.id]
54
+ );
55
+
56
+ const totalRequests = parseInt(usage?.total_requests || '0');
57
+ const totalErrors = parseInt(usage?.total_errors || '0');
58
+ const twins = parseInt(twinCount?.count || '0');
59
+
60
+ res.json({
61
+ tier: user.tier,
62
+ period: {
63
+ start: startOfMonth.toISOString().split('T')[0],
64
+ end: endOfMonth.toISOString().split('T')[0],
65
+ },
66
+ twins: {
67
+ used: twins,
68
+ limit: limits.twins === -1 ? 'unlimited' : limits.twins,
69
+ },
70
+ requests: {
71
+ used: totalRequests,
72
+ limit: limits.requestsPerMonth === -1 ? 'unlimited' : limits.requestsPerMonth,
73
+ percentUsed: limits.requestsPerMonth === -1 ? 0 : Math.round((totalRequests / limits.requestsPerMonth) * 100),
74
+ },
75
+ errors: {
76
+ count: totalErrors,
77
+ rate: totalRequests > 0 ? Math.round((totalErrors / totalRequests) * 100) : 0,
78
+ },
79
+ avgLatencyMs: totalRequests > 0
80
+ ? Math.round(parseInt(usage?.total_duration || '0') / totalRequests)
81
+ : 0,
82
+ });
83
+ } catch (error: any) {
84
+ console.error('[Usage] Get error:', error);
85
+ res.status(500).json({
86
+ error: {
87
+ code: 'INTERNAL_ERROR',
88
+ message: 'Failed to get usage',
89
+ },
90
+ });
91
+ }
92
+ });
93
+
94
+ /**
95
+ * GET /api/usage/history
96
+ * Get usage history (daily breakdown)
97
+ */
98
+ router.get('/history', authenticateApiKey, async (req: Request, res: Response) => {
99
+ try {
100
+ const { days = '30' } = req.query;
101
+ const daysNum = Math.min(parseInt(days as string) || 30, 90);
102
+
103
+ const startDate = new Date();
104
+ startDate.setDate(startDate.getDate() - daysNum);
105
+
106
+ const usage = await query<UsageDaily>(
107
+ `SELECT date, request_count, error_count, total_duration_ms
108
+ FROM usage_daily
109
+ WHERE user_id = $1 AND date >= $2
110
+ ORDER BY date DESC`,
111
+ [req.user!.id, startDate.toISOString().split('T')[0]]
112
+ );
113
+
114
+ res.json({
115
+ history: usage.map((u) => ({
116
+ date: u.date,
117
+ requests: u.request_count,
118
+ errors: u.error_count,
119
+ avgLatencyMs: u.request_count > 0
120
+ ? Math.round(u.total_duration_ms / u.request_count)
121
+ : 0,
122
+ })),
123
+ });
124
+ } catch (error: any) {
125
+ console.error('[Usage] Get history error:', error);
126
+ res.status(500).json({
127
+ error: {
128
+ code: 'INTERNAL_ERROR',
129
+ message: 'Failed to get usage history',
130
+ },
131
+ });
132
+ }
133
+ });
134
+
135
+ /**
136
+ * GET /api/usage/by-twin
137
+ * Get usage breakdown by twin
138
+ */
139
+ router.get('/by-twin', authenticateApiKey, async (req: Request, res: Response) => {
140
+ try {
141
+ // Get current month
142
+ const now = new Date();
143
+ const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
144
+
145
+ const usage = await query<{
146
+ twin_id: string;
147
+ twin_name: string;
148
+ request_count: string;
149
+ error_count: string;
150
+ avg_duration: string;
151
+ }>(
152
+ `SELECT
153
+ t.id as twin_id,
154
+ t.name as twin_name,
155
+ COUNT(l.id) as request_count,
156
+ SUM(CASE WHEN l.status = 'error' THEN 1 ELSE 0 END) as error_count,
157
+ AVG(l.duration_ms) as avg_duration
158
+ FROM twins t
159
+ LEFT JOIN logs l ON l.twin_id = t.id AND l.created_at >= $2
160
+ WHERE t.user_id = $1
161
+ GROUP BY t.id, t.name
162
+ ORDER BY request_count DESC`,
163
+ [req.user!.id, startOfMonth.toISOString()]
164
+ );
165
+
166
+ res.json({
167
+ byTwin: usage.map((u) => ({
168
+ twinId: u.twin_id,
169
+ twinName: u.twin_name,
170
+ requests: parseInt(u.request_count || '0'),
171
+ errors: parseInt(u.error_count || '0'),
172
+ avgLatencyMs: Math.round(parseFloat(u.avg_duration || '0')),
173
+ })),
174
+ });
175
+ } catch (error: any) {
176
+ console.error('[Usage] Get by twin error:', error);
177
+ res.status(500).json({
178
+ error: {
179
+ code: 'INTERNAL_ERROR',
180
+ message: 'Failed to get usage by twin',
181
+ },
182
+ });
183
+ }
184
+ });
185
+
186
+ export default router;
@@ -0,0 +1,151 @@
1
+ /**
2
+ * MCP Twin Cloud Server
3
+ * Zero-downtime MCP server management as a service
4
+ *
5
+ * Powered by Prax Chat - https://prax.chat
6
+ */
7
+
8
+ import express, { Request, Response, NextFunction } from 'express';
9
+ import cors from 'cors';
10
+ import helmet from 'helmet';
11
+ import { initializeDatabase, closePool } from './db';
12
+ import authRoutes from './routes/auth';
13
+ import twinRoutes from './routes/twins';
14
+ import usageRoutes from './routes/usage';
15
+
16
+ const app = express();
17
+ const PORT = parseInt(process.env.PORT || '3000');
18
+ const HOST = process.env.HOST || '0.0.0.0';
19
+
20
+ // Middleware
21
+ app.use(helmet());
22
+ app.use(cors({
23
+ origin: process.env.CORS_ORIGIN || '*',
24
+ credentials: true,
25
+ }));
26
+ app.use(express.json({ limit: '10mb' }));
27
+
28
+ // Request logging
29
+ app.use((req: Request, res: Response, next: NextFunction) => {
30
+ const start = Date.now();
31
+ res.on('finish', () => {
32
+ const duration = Date.now() - start;
33
+ if (process.env.DEBUG_HTTP) {
34
+ console.log(`[HTTP] ${req.method} ${req.path} ${res.statusCode} ${duration}ms`);
35
+ }
36
+ });
37
+ next();
38
+ });
39
+
40
+ // Health check
41
+ app.get('/health', (req: Request, res: Response) => {
42
+ res.json({
43
+ status: 'healthy',
44
+ service: 'mcp-twin-cloud',
45
+ version: '1.2.0',
46
+ timestamp: new Date().toISOString(),
47
+ });
48
+ });
49
+
50
+ // API Routes
51
+ app.use('/api/auth', authRoutes);
52
+ app.use('/api/twins', twinRoutes);
53
+ app.use('/api/usage', usageRoutes);
54
+
55
+ // API documentation
56
+ app.get('/api', (req: Request, res: Response) => {
57
+ res.json({
58
+ name: 'MCP Twin Cloud API',
59
+ version: '1.0.0',
60
+ documentation: 'https://prax.chat/mcp-twin/docs',
61
+ endpoints: {
62
+ auth: {
63
+ 'POST /api/auth/signup': 'Create account',
64
+ 'POST /api/auth/login': 'Login and get session token',
65
+ 'GET /api/auth/me': 'Get current user info',
66
+ 'POST /api/auth/api-keys': 'Create API key',
67
+ 'GET /api/auth/api-keys': 'List API keys',
68
+ 'DELETE /api/auth/api-keys/:id': 'Revoke API key',
69
+ },
70
+ twins: {
71
+ 'POST /api/twins': 'Create twin',
72
+ 'GET /api/twins': 'List twins',
73
+ 'GET /api/twins/:id': 'Get twin details',
74
+ 'PATCH /api/twins/:id': 'Update twin',
75
+ 'DELETE /api/twins/:id': 'Delete twin',
76
+ 'POST /api/twins/:id/start': 'Start twin',
77
+ 'POST /api/twins/:id/stop': 'Stop twin',
78
+ 'POST /api/twins/:id/reload': 'Reload standby',
79
+ 'POST /api/twins/:id/swap': 'Swap active server',
80
+ 'POST /api/twins/:id/call': 'Execute tool call',
81
+ 'GET /api/twins/:id/logs': 'Get execution logs',
82
+ 'GET /api/twins/:id/status': 'Get health status',
83
+ },
84
+ usage: {
85
+ 'GET /api/usage': 'Get current month usage',
86
+ 'GET /api/usage/history': 'Get usage history',
87
+ 'GET /api/usage/by-twin': 'Get usage by twin',
88
+ },
89
+ },
90
+ });
91
+ });
92
+
93
+ // 404 handler
94
+ app.use((req: Request, res: Response) => {
95
+ res.status(404).json({
96
+ error: {
97
+ code: 'NOT_FOUND',
98
+ message: `Route ${req.method} ${req.path} not found`,
99
+ },
100
+ });
101
+ });
102
+
103
+ // Error handler
104
+ app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
105
+ console.error('[Server] Unhandled error:', err);
106
+ res.status(500).json({
107
+ error: {
108
+ code: 'INTERNAL_ERROR',
109
+ message: 'An unexpected error occurred',
110
+ },
111
+ });
112
+ });
113
+
114
+ // Start server
115
+ export async function startServer(): Promise<void> {
116
+ console.log('─────────────────────────────────────────');
117
+ console.log('⚡ MCP Twin Cloud');
118
+ console.log(' Powered by Prax Chat');
119
+ console.log('─────────────────────────────────────────');
120
+ console.log('');
121
+
122
+ // Initialize database
123
+ await initializeDatabase();
124
+ console.log('[Server] Database initialized');
125
+
126
+ // Start listening
127
+ app.listen(PORT, HOST, () => {
128
+ console.log(`[Server] Listening on http://${HOST}:${PORT}`);
129
+ console.log('');
130
+ console.log('API Endpoints:');
131
+ console.log(` Health: http://${HOST}:${PORT}/health`);
132
+ console.log(` Docs: http://${HOST}:${PORT}/api`);
133
+ console.log(` Auth: http://${HOST}:${PORT}/api/auth/*`);
134
+ console.log(` Twins: http://${HOST}:${PORT}/api/twins/*`);
135
+ console.log(` Usage: http://${HOST}:${PORT}/api/usage/*`);
136
+ console.log('');
137
+ console.log('─────────────────────────────────────────');
138
+ });
139
+
140
+ // Graceful shutdown
141
+ const shutdown = async () => {
142
+ console.log('\n[Server] Shutting down...');
143
+ await closePool();
144
+ process.exit(0);
145
+ };
146
+
147
+ process.on('SIGINT', shutdown);
148
+ process.on('SIGTERM', shutdown);
149
+ }
150
+
151
+ export default app;