Migración desde trading-platform/apps/mcp-vip - Estándar multi-repo v2

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
rckrdmrd 2026-01-16 08:33:18 -06:00
commit 41952f8985
14 changed files with 1943 additions and 0 deletions

25
.env.example Normal file
View File

@ -0,0 +1,25 @@
# MCP VIP SERVER CONFIGURATION
PORT=3092
NODE_ENV=development
LOG_LEVEL=info
# Database
DB_HOST=localhost
DB_PORT=5432
DB_NAME=trading_platform
DB_USER=trading_app
DB_PASSWORD=your_password
DB_SSL=false
DB_POOL_MAX=20
# VIP Config
VIP_TRIAL_DAYS=7
VIP_GRACE_PERIOD_DAYS=3
# Wallet Service
WALLET_SERVICE_URL=http://localhost:3090
# Stripe
STRIPE_SECRET_KEY=sk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...

18
Dockerfile Normal file
View File

@ -0,0 +1,18 @@
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY tsconfig.json ./
COPY src ./src
RUN npm run build && npm prune --production
FROM node:20-alpine
WORKDIR /app
RUN addgroup -g 1001 -S nodejs && adduser -S vip -u 1001
COPY --from=builder --chown=vip:nodejs /app/node_modules ./node_modules
COPY --from=builder --chown=vip:nodejs /app/dist ./dist
COPY --from=builder --chown=vip:nodejs /app/package.json ./
USER vip
EXPOSE 3092
HEALTHCHECK --interval=30s --timeout=10s CMD wget -q --spider http://localhost:3092/health || exit 1
CMD ["node", "dist/index.js"]

68
README.md Normal file
View File

@ -0,0 +1,68 @@
# MCP VIP Server
VIP Subscription MCP Server for the Trading Platform. Manages Gold, Platinum, Diamond tiers with exclusive ML model access.
## VIP Tiers
| Tier | Price/mo | Predictions | Models |
|------|----------|-------------|--------|
| **Gold** | $199 | 50/mo | AMD, Range |
| **Platinum** | $399 | 150/mo | + TPSL, ICT/SMC |
| **Diamond** | $999 | Unlimited | + Strategy Ensemble |
## Quick Start
```bash
npm install
cp .env.example .env
npm run dev
```
## API Endpoints
### Tiers
- `GET /api/v1/tiers` - List all tiers
- `GET /api/v1/tiers/:tier` - Get tier details
### Subscriptions
- `POST /api/v1/subscriptions` - Create subscription
- `GET /api/v1/subscriptions/:id` - Get subscription
- `GET /api/v1/users/:userId/subscription` - Get user subscription
- `POST /api/v1/subscriptions/:id/cancel` - Cancel
- `POST /api/v1/subscriptions/:id/reactivate` - Reactivate
- `POST /api/v1/subscriptions/:id/upgrade` - Upgrade tier
- `GET /api/v1/subscriptions/:id/usage` - Usage stats
- `GET /api/v1/subscriptions/:id/models` - Model access
### Model Access
- `GET /api/v1/users/:userId/access/:modelId` - Check access
## MCP Tools (13)
| Tool | Description |
|------|-------------|
| `vip_get_tiers` | List tiers |
| `vip_get_tier` | Get tier |
| `vip_create_subscription` | Create sub |
| `vip_get_subscription` | Get sub |
| `vip_cancel_subscription` | Cancel |
| `vip_reactivate_subscription` | Reactivate |
| `vip_upgrade_tier` | Upgrade |
| `vip_check_model_access` | Check access |
| `vip_get_model_access` | List access |
| `vip_record_model_usage` | Record usage |
| `vip_get_usage_stats` | Usage stats |
| `vip_update_status` | Update status |
| `vip_renew_period` | Renew period |
## Subscription Status
- `trialing` - In trial period
- `active` - Active and paying
- `past_due` - Payment failed
- `cancelled` - Cancelled
- `expired` - Expired
## License
UNLICENSED - Private

42
package.json Normal file
View File

@ -0,0 +1,42 @@
{
"name": "@trading-platform/mcp-vip",
"version": "1.0.0",
"description": "MCP Server for VIP Subscriptions - Gold, Platinum, Diamond tiers",
"main": "dist/index.js",
"scripts": {
"dev": "ts-node-dev --respawn --transpile-only src/index.ts",
"build": "tsc",
"start": "node dist/index.js",
"lint": "eslint src --ext .ts",
"typecheck": "tsc --noEmit",
"test": "jest"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.0.0",
"express": "^4.18.2",
"pg": "^8.11.3",
"zod": "^3.22.4",
"winston": "^3.11.0",
"decimal.js": "^10.4.3",
"uuid": "^9.0.1",
"dotenv": "^16.3.1",
"helmet": "^7.1.0",
"cors": "^2.8.5",
"jsonwebtoken": "^9.0.2"
},
"devDependencies": {
"@types/express": "^4.17.21",
"@types/node": "^20.10.0",
"@types/pg": "^8.10.9",
"@types/uuid": "^9.0.7",
"@types/cors": "^2.8.17",
"@types/jsonwebtoken": "^9.0.5",
"typescript": "^5.3.2",
"ts-node-dev": "^2.0.0",
"jest": "^29.7.0",
"@types/jest": "^29.5.11",
"ts-jest": "^29.1.1"
},
"engines": { "node": ">=18.0.0" },
"private": true
}

55
src/config.ts Normal file
View File

@ -0,0 +1,55 @@
import { Pool, PoolConfig } from 'pg';
import dotenv from 'dotenv';
dotenv.config();
export const serverConfig = {
port: parseInt(process.env.PORT || '3092', 10),
nodeEnv: process.env.NODE_ENV || 'development',
logLevel: process.env.LOG_LEVEL || 'info',
};
export const dbConfig: PoolConfig = {
host: process.env.DB_HOST || 'localhost',
port: parseInt(process.env.DB_PORT || '5432', 10),
database: process.env.DB_NAME || 'trading_platform',
user: process.env.DB_USER || 'trading_app',
password: process.env.DB_PASSWORD || '',
ssl: process.env.DB_SSL === 'true' ? { rejectUnauthorized: false } : false,
max: parseInt(process.env.DB_POOL_MAX || '20', 10),
};
export const vipConfig = {
trialDays: parseInt(process.env.VIP_TRIAL_DAYS || '7', 10),
gracePeriodDays: parseInt(process.env.VIP_GRACE_PERIOD_DAYS || '3', 10),
};
export const walletServiceConfig = {
baseUrl: process.env.WALLET_SERVICE_URL || 'http://localhost:3090',
};
export const stripeConfig = {
secretKey: process.env.STRIPE_SECRET_KEY || '',
webhookSecret: process.env.STRIPE_WEBHOOK_SECRET || '',
};
let pool: Pool | null = null;
export function getPool(): Pool {
if (!pool) {
pool = new Pool(dbConfig);
pool.on('error', (err) => console.error('DB pool error', err));
}
return pool;
}
export async function closePool(): Promise<void> {
if (pool) { await pool.end(); pool = null; }
}
export async function setTenantContext(
client: ReturnType<Pool['connect']> extends Promise<infer T> ? T : never,
tenantId: string
): Promise<void> {
await client.query(`SET app.current_tenant_id = $1`, [tenantId]);
}

292
src/index.ts Normal file
View File

@ -0,0 +1,292 @@
/**
* MCP VIP Server
*
* VIP Subscription system for trading platform.
* Manages Gold, Platinum, Diamond tiers and exclusive model access.
*/
import express, { Request, Response, NextFunction } from 'express';
import helmet from 'helmet';
import cors from 'cors';
import { ZodError } from 'zod';
import { serverConfig, closePool, getPool } from './config';
import { logger } from './utils/logger';
import { allToolSchemas, toolHandlers, toolNames } from './tools';
import { authMiddleware } from './middleware';
const app = express();
app.use(helmet());
app.use(cors());
app.use(express.json({ limit: '1mb' }));
// ============================================================================
// HEALTH & INFO
// ============================================================================
app.get('/health', async (_req: Request, res: Response) => {
try {
await getPool().query('SELECT 1');
res.json({ status: 'healthy', service: 'mcp-vip', database: 'connected' });
} catch {
res.status(503).json({ status: 'unhealthy', service: 'mcp-vip', database: 'disconnected' });
}
});
app.get('/info', (_req: Request, res: Response) => {
res.json({ name: 'mcp-vip', version: '1.0.0', tools: toolNames, toolCount: toolNames.length });
});
// ============================================================================
// MCP ENDPOINTS
// ============================================================================
app.get('/mcp/tools', (_req: Request, res: Response) => {
res.json({ tools: Object.values(allToolSchemas) });
});
app.post('/mcp/tools/:toolName', async (req: Request, res: Response, next: NextFunction) => {
const handler = toolHandlers[req.params.toolName];
if (!handler) {
res.status(404).json({ error: `Tool not found: ${req.params.toolName}` });
return;
}
try {
res.json(await handler(req.body));
} catch (error) {
next(error);
}
});
app.post('/mcp/call', async (req: Request, res: Response, next: NextFunction) => {
const { name, arguments: args } = req.body;
const handler = toolHandlers[name];
if (!handler) {
res.status(404).json({ error: `Tool not found: ${name}` });
return;
}
try {
res.json(await handler(args || {}));
} catch (error) {
next(error);
}
});
// ============================================================================
// REST API ENDPOINTS
// ============================================================================
// Tiers
app.get('/api/v1/tiers', async (_req: Request, res: Response, next: NextFunction) => {
try {
res.json(await toolHandlers.vip_get_tiers({}));
} catch (error) {
next(error);
}
});
app.get('/api/v1/tiers/:tier', async (req: Request, res: Response, next: NextFunction) => {
try {
res.json(await toolHandlers.vip_get_tier({ tier: req.params.tier }));
} catch (error) {
next(error);
}
});
// Subscriptions - Protected routes
const subscriptionsRouter = express.Router();
subscriptionsRouter.use(authMiddleware);
// Create subscription
subscriptionsRouter.post('/', async (req: Request, res: Response, next: NextFunction) => {
try {
res.status(201).json(await toolHandlers.vip_create_subscription({
tenantId: req.tenantId,
userId: req.userId,
...req.body,
}));
} catch (error) {
next(error);
}
});
// Get subscription by ID
subscriptionsRouter.get('/:subscriptionId', async (req: Request, res: Response, next: NextFunction) => {
try {
res.json(await toolHandlers.vip_get_subscription({
tenantId: req.tenantId,
subscriptionId: req.params.subscriptionId,
}));
} catch (error) {
next(error);
}
});
// Cancel subscription
subscriptionsRouter.post('/:subscriptionId/cancel', async (req: Request, res: Response, next: NextFunction) => {
try {
res.json(await toolHandlers.vip_cancel_subscription({
tenantId: req.tenantId,
subscriptionId: req.params.subscriptionId,
cancelledBy: req.userId,
...req.body,
}));
} catch (error) {
next(error);
}
});
// Reactivate subscription
subscriptionsRouter.post('/:subscriptionId/reactivate', async (req: Request, res: Response, next: NextFunction) => {
try {
res.json(await toolHandlers.vip_reactivate_subscription({
tenantId: req.tenantId,
subscriptionId: req.params.subscriptionId,
}));
} catch (error) {
next(error);
}
});
// Upgrade tier
subscriptionsRouter.post('/:subscriptionId/upgrade', async (req: Request, res: Response, next: NextFunction) => {
try {
res.json(await toolHandlers.vip_upgrade_tier({
tenantId: req.tenantId,
subscriptionId: req.params.subscriptionId,
newTier: req.body.newTier,
}));
} catch (error) {
next(error);
}
});
// Get usage stats
subscriptionsRouter.get('/:subscriptionId/usage', async (req: Request, res: Response, next: NextFunction) => {
try {
res.json(await toolHandlers.vip_get_usage_stats({
tenantId: req.tenantId,
subscriptionId: req.params.subscriptionId,
}));
} catch (error) {
next(error);
}
});
// Get model access
subscriptionsRouter.get('/:subscriptionId/models', async (req: Request, res: Response, next: NextFunction) => {
try {
res.json(await toolHandlers.vip_get_model_access({
tenantId: req.tenantId,
subscriptionId: req.params.subscriptionId,
}));
} catch (error) {
next(error);
}
});
app.use('/api/v1/subscriptions', subscriptionsRouter);
// User-specific endpoints - Protected
const userRouter = express.Router();
userRouter.use(authMiddleware);
// Get user's subscription
userRouter.get('/:userId/subscription', async (req: Request, res: Response, next: NextFunction) => {
try {
res.json(await toolHandlers.vip_get_subscription({
tenantId: req.tenantId,
userId: req.params.userId,
}));
} catch (error) {
next(error);
}
});
// Check model access
userRouter.get('/:userId/access/:modelId', async (req: Request, res: Response, next: NextFunction) => {
try {
res.json(await toolHandlers.vip_check_model_access({
tenantId: req.tenantId,
userId: req.params.userId,
modelId: req.params.modelId,
}));
} catch (error) {
next(error);
}
});
// Get current user's subscription (convenience)
userRouter.get('/me/subscription', async (req: Request, res: Response, next: NextFunction) => {
try {
res.json(await toolHandlers.vip_get_subscription({
tenantId: req.tenantId,
userId: req.userId,
}));
} catch (error) {
next(error);
}
});
// Check current user's model access
userRouter.get('/me/access/:modelId', async (req: Request, res: Response, next: NextFunction) => {
try {
res.json(await toolHandlers.vip_check_model_access({
tenantId: req.tenantId,
userId: req.userId,
modelId: req.params.modelId,
}));
} catch (error) {
next(error);
}
});
app.use('/api/v1/users', userRouter);
// ============================================================================
// ERROR HANDLING
// ============================================================================
app.use((_req: Request, res: Response) => {
res.status(404).json({ error: 'Not found' });
});
app.use((err: Error, _req: Request, res: Response, _next: NextFunction) => {
if (err instanceof ZodError) {
res.status(400).json({ error: 'Validation error', details: err.errors });
return;
}
logger.error('Unhandled error', { error: err.message, stack: err.stack });
res.status(500).json({ error: 'Internal server error' });
});
// ============================================================================
// SERVER STARTUP
// ============================================================================
const server = app.listen(serverConfig.port, () => {
logger.info(`MCP VIP Server started`, { port: serverConfig.port });
console.log(`
MCP VIP SERVER
Port: ${serverConfig.port}
Tools: ${String(toolNames.length).padEnd(12)}
Tiers: GOLD | PLATINUM | DIAMOND
/api/v1/tiers, /api/v1/subscriptions/*
`);
});
const shutdown = async (signal: string) => {
logger.info(`Received ${signal}, shutting down...`);
server.close(async () => { await closePool(); process.exit(0); });
setTimeout(() => process.exit(1), 30000);
};
process.on('SIGTERM', () => shutdown('SIGTERM'));
process.on('SIGINT', () => shutdown('SIGINT'));
export { app };

View File

@ -0,0 +1,99 @@
/**
* Auth Middleware for MCP VIP
* Verifies JWT tokens and sets user context
*/
import { Request, Response, NextFunction } from 'express';
import jwt from 'jsonwebtoken';
import { logger } from '../utils/logger';
const JWT_SECRET = process.env.JWT_SECRET || 'dev-jwt-secret-change-in-production-min-256-bits';
export interface JWTPayload {
sub: string;
email: string;
tenantId: string;
isOwner: boolean;
iat: number;
exp: number;
}
declare global {
namespace Express {
interface Request {
userId?: string;
tenantId?: string;
userEmail?: string;
isOwner?: boolean;
isAuthenticated?: boolean;
}
}
}
export function authMiddleware(req: Request, res: Response, next: NextFunction): void {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
res.status(401).json({
error: 'Unauthorized',
code: 'MISSING_TOKEN',
message: 'No authentication token provided',
});
return;
}
const token = authHeader.substring(7);
try {
const decoded = jwt.verify(token, JWT_SECRET) as JWTPayload;
req.userId = decoded.sub;
req.tenantId = decoded.tenantId;
req.userEmail = decoded.email;
req.isOwner = decoded.isOwner;
req.isAuthenticated = true;
req.headers['x-tenant-id'] = decoded.tenantId;
req.headers['x-user-id'] = decoded.sub;
next();
} catch (error) {
if (error instanceof jwt.TokenExpiredError) {
res.status(401).json({ error: 'Unauthorized', code: 'TOKEN_EXPIRED' });
return;
}
if (error instanceof jwt.JsonWebTokenError) {
res.status(401).json({ error: 'Unauthorized', code: 'INVALID_TOKEN' });
return;
}
logger.error('Auth middleware error', { error });
res.status(500).json({ error: 'Internal server error', code: 'AUTH_ERROR' });
}
}
export function optionalAuthMiddleware(req: Request, _res: Response, next: NextFunction): void {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
req.isAuthenticated = false;
next();
return;
}
const token = authHeader.substring(7);
try {
const decoded = jwt.verify(token, JWT_SECRET) as JWTPayload;
req.userId = decoded.sub;
req.tenantId = decoded.tenantId;
req.userEmail = decoded.email;
req.isOwner = decoded.isOwner;
req.isAuthenticated = true;
req.headers['x-tenant-id'] = decoded.tenantId;
req.headers['x-user-id'] = decoded.sub;
} catch {
req.isAuthenticated = false;
}
next();
}

1
src/middleware/index.ts Normal file
View File

@ -0,0 +1 @@
export { authMiddleware, optionalAuthMiddleware } from './auth.middleware';

541
src/services/vip.service.ts Normal file
View File

@ -0,0 +1,541 @@
import { Pool } from 'pg';
import Decimal from 'decimal.js';
import { getPool, setTenantContext, vipConfig, walletServiceConfig } from '../config';
import {
VipTier,
VipSubscription,
SubscriptionWithTier,
ModelAccess,
VipTierType,
SubscriptionStatus,
CreateSubscriptionInput,
UsageStats,
mapRowToTier,
mapRowToSubscription,
mapRowToModelAccess,
} from '../types/vip.types';
export class VipService {
private pool: Pool;
constructor() {
this.pool = getPool();
}
// ==================== TIERS ====================
async getAllTiers(): Promise<VipTier[]> {
const result = await this.pool.query(
'SELECT * FROM vip.tiers WHERE is_active = TRUE ORDER BY sort_order ASC'
);
return result.rows.map(mapRowToTier);
}
async getTierById(tierId: string): Promise<VipTier | null> {
const result = await this.pool.query('SELECT * FROM vip.tiers WHERE id = $1', [tierId]);
return result.rows.length > 0 ? mapRowToTier(result.rows[0]) : null;
}
async getTierByType(tier: VipTierType): Promise<VipTier | null> {
const result = await this.pool.query(
'SELECT * FROM vip.tiers WHERE tier = $1 AND is_active = TRUE',
[tier]
);
return result.rows.length > 0 ? mapRowToTier(result.rows[0]) : null;
}
// ==================== SUBSCRIPTIONS ====================
async createSubscription(input: CreateSubscriptionInput): Promise<SubscriptionWithTier> {
const client = await this.pool.connect();
try {
await client.query('BEGIN');
await setTenantContext(client, input.tenantId);
const tier = await this.getTierByType(input.tier);
if (!tier) {
throw new Error(`Tier not found: ${input.tier}`);
}
// Check for existing active subscription
const existing = await client.query(
`SELECT id FROM vip.subscriptions
WHERE tenant_id = $1 AND user_id = $2 AND status IN ('active', 'trialing')`,
[input.tenantId, input.userId]
);
if (existing.rows.length > 0) {
throw new Error('User already has an active VIP subscription');
}
const now = new Date();
const trialEnd = input.startTrial
? new Date(now.getTime() + vipConfig.trialDays * 24 * 60 * 60 * 1000)
: null;
const periodEnd = new Date(
now.getTime() + (input.interval === 'year' ? 365 : 30) * 24 * 60 * 60 * 1000
);
const result = await client.query(
`INSERT INTO vip.subscriptions (
tenant_id, user_id, tier_id,
stripe_subscription_id, stripe_customer_id,
status, interval,
current_period_start, current_period_end,
trial_start, trial_end
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
RETURNING *`,
[
input.tenantId,
input.userId,
tier.id,
input.stripeSubscriptionId || null,
input.stripeCustomerId || null,
input.startTrial ? 'trialing' : 'active',
input.interval || 'month',
now,
periodEnd,
input.startTrial ? now : null,
trialEnd,
]
);
const subscription = mapRowToSubscription(result.rows[0]);
// Grant model access
for (const modelId of tier.includedModels) {
await client.query(
`INSERT INTO vip.model_access (subscription_id, model_id, model_name)
VALUES ($1, $2, $3)`,
[subscription.id, modelId, modelId]
);
}
await client.query('COMMIT');
return { ...subscription, tier };
} catch (error) {
await client.query('ROLLBACK');
throw error;
} finally {
client.release();
}
}
async getSubscription(userId: string, tenantId: string): Promise<SubscriptionWithTier | null> {
const client = await this.pool.connect();
try {
await setTenantContext(client, tenantId);
const result = await client.query(
`SELECT s.*, t.*,
s.id as subscription_id, t.id as tier_id
FROM vip.subscriptions s
JOIN vip.tiers t ON s.tier_id = t.id
WHERE s.user_id = $1 AND s.status IN ('active', 'trialing', 'past_due')
LIMIT 1`,
[userId]
);
if (result.rows.length === 0) return null;
const row = result.rows[0];
const subscription = mapRowToSubscription({ ...row, id: row.subscription_id });
const tier = mapRowToTier({ ...row, id: row.tier_id });
return { ...subscription, tier };
} finally {
client.release();
}
}
async getSubscriptionById(subscriptionId: string, tenantId: string): Promise<SubscriptionWithTier | null> {
const client = await this.pool.connect();
try {
await setTenantContext(client, tenantId);
const result = await client.query(
`SELECT s.*, t.*,
s.id as subscription_id, t.id as tier_id
FROM vip.subscriptions s
JOIN vip.tiers t ON s.tier_id = t.id
WHERE s.id = $1`,
[subscriptionId]
);
if (result.rows.length === 0) return null;
const row = result.rows[0];
const subscription = mapRowToSubscription({ ...row, id: row.subscription_id });
const tier = mapRowToTier({ ...row, id: row.tier_id });
return { ...subscription, tier };
} finally {
client.release();
}
}
async cancelSubscription(
subscriptionId: string,
tenantId: string,
reason?: string,
cancelImmediately: boolean = false
): Promise<VipSubscription> {
const client = await this.pool.connect();
try {
await setTenantContext(client, tenantId);
const cancelAt = cancelImmediately ? new Date() : null;
const newStatus = cancelImmediately ? 'cancelled' : 'active';
const result = await client.query(
`UPDATE vip.subscriptions
SET status = $1,
cancel_at = COALESCE($2, current_period_end),
cancelled_at = NOW(),
cancel_reason = $3
WHERE id = $4
RETURNING *`,
[newStatus, cancelAt, reason || null, subscriptionId]
);
if (result.rows.length === 0) {
throw new Error('Subscription not found');
}
return mapRowToSubscription(result.rows[0]);
} finally {
client.release();
}
}
async reactivateSubscription(subscriptionId: string, tenantId: string): Promise<VipSubscription> {
const client = await this.pool.connect();
try {
await setTenantContext(client, tenantId);
const result = await client.query(
`UPDATE vip.subscriptions
SET status = 'active',
cancel_at = NULL,
cancelled_at = NULL,
cancel_reason = NULL
WHERE id = $1 AND status IN ('cancelled', 'past_due')
RETURNING *`,
[subscriptionId]
);
if (result.rows.length === 0) {
throw new Error('Subscription not found or cannot be reactivated');
}
return mapRowToSubscription(result.rows[0]);
} finally {
client.release();
}
}
async upgradeTier(
subscriptionId: string,
newTier: VipTierType,
tenantId: string
): Promise<SubscriptionWithTier> {
const client = await this.pool.connect();
try {
await client.query('BEGIN');
await setTenantContext(client, tenantId);
const tier = await this.getTierByType(newTier);
if (!tier) {
throw new Error(`Tier not found: ${newTier}`);
}
// Update subscription
const result = await client.query(
`UPDATE vip.subscriptions SET tier_id = $1 WHERE id = $2 RETURNING *`,
[tier.id, subscriptionId]
);
if (result.rows.length === 0) {
throw new Error('Subscription not found');
}
const subscription = mapRowToSubscription(result.rows[0]);
// Update model access
await client.query(
'DELETE FROM vip.model_access WHERE subscription_id = $1',
[subscriptionId]
);
for (const modelId of tier.includedModels) {
await client.query(
`INSERT INTO vip.model_access (subscription_id, model_id, model_name)
VALUES ($1, $2, $3)`,
[subscriptionId, modelId, modelId]
);
}
await client.query('COMMIT');
return { ...subscription, tier };
} catch (error) {
await client.query('ROLLBACK');
throw error;
} finally {
client.release();
}
}
// ==================== MODEL ACCESS ====================
async checkModelAccess(userId: string, modelId: string, tenantId: string): Promise<boolean> {
const client = await this.pool.connect();
try {
await setTenantContext(client, tenantId);
const result = await client.query(
`SELECT vip.check_vip_access($1, $2) as has_access`,
[userId, modelId]
);
return result.rows[0]?.has_access || false;
} finally {
client.release();
}
}
async getModelAccess(subscriptionId: string, tenantId: string): Promise<ModelAccess[]> {
const client = await this.pool.connect();
try {
await setTenantContext(client, tenantId);
const result = await client.query(
`SELECT * FROM vip.model_access WHERE subscription_id = $1`,
[subscriptionId]
);
return result.rows.map(mapRowToModelAccess);
} finally {
client.release();
}
}
async recordModelUsage(
subscriptionId: string,
modelId: string,
tenantId: string
): Promise<ModelAccess> {
const client = await this.pool.connect();
try {
await setTenantContext(client, tenantId);
const result = await client.query(
`UPDATE vip.model_access
SET predictions_generated = predictions_generated + 1,
daily_used = daily_used + 1,
monthly_used = monthly_used + 1,
last_used_at = NOW()
WHERE subscription_id = $1 AND model_id = $2
RETURNING *`,
[subscriptionId, modelId]
);
if (result.rows.length === 0) {
throw new Error('Model access not found');
}
// Also update subscription usage
await client.query(
`UPDATE vip.subscriptions
SET vip_predictions_used = vip_predictions_used + 1
WHERE id = $1`,
[subscriptionId]
);
return mapRowToModelAccess(result.rows[0]);
} finally {
client.release();
}
}
// ==================== USAGE ====================
async getUsageStats(subscriptionId: string, tenantId: string): Promise<UsageStats | null> {
const client = await this.pool.connect();
try {
await setTenantContext(client, tenantId);
const result = await client.query(
`SELECT s.*, t.limits
FROM vip.subscriptions s
JOIN vip.tiers t ON s.tier_id = t.id
WHERE s.id = $1`,
[subscriptionId]
);
if (result.rows.length === 0) return null;
const row = result.rows[0];
const limits = row.limits as { vip_predictions_per_month: number; api_calls_per_month: number };
const periodEnd = new Date(row.current_period_end);
const now = new Date();
const daysRemaining = Math.max(0, Math.ceil((periodEnd.getTime() - now.getTime()) / (24 * 60 * 60 * 1000)));
const predictionsLimit = limits.vip_predictions_per_month === -1 ? Infinity : limits.vip_predictions_per_month;
const apiLimit = limits.api_calls_per_month === -1 ? Infinity : limits.api_calls_per_month;
return {
predictionsUsed: row.vip_predictions_used,
predictionsLimit,
predictionsRemaining: Math.max(0, predictionsLimit - row.vip_predictions_used),
apiCallsUsed: row.api_calls_this_period,
apiCallsLimit: apiLimit,
apiCallsRemaining: Math.max(0, apiLimit - row.api_calls_this_period),
periodStart: new Date(row.current_period_start),
periodEnd,
daysRemaining,
};
} finally {
client.release();
}
}
async incrementApiCalls(subscriptionId: string, tenantId: string): Promise<number> {
const client = await this.pool.connect();
try {
await setTenantContext(client, tenantId);
const result = await client.query(
`UPDATE vip.subscriptions
SET api_calls_this_period = api_calls_this_period + 1
WHERE id = $1
RETURNING api_calls_this_period`,
[subscriptionId]
);
return result.rows[0]?.api_calls_this_period || 0;
} finally {
client.release();
}
}
async resetUsage(subscriptionId: string, tenantId: string): Promise<void> {
const client = await this.pool.connect();
try {
await setTenantContext(client, tenantId);
await client.query(
`UPDATE vip.subscriptions
SET vip_predictions_used = 0,
api_calls_this_period = 0,
last_usage_reset = NOW()
WHERE id = $1`,
[subscriptionId]
);
await client.query(
`UPDATE vip.model_access
SET daily_used = 0, monthly_used = 0
WHERE subscription_id = $1`,
[subscriptionId]
);
} finally {
client.release();
}
}
// ==================== STATUS MANAGEMENT ====================
async updateStatus(
subscriptionId: string,
status: SubscriptionStatus,
tenantId: string
): Promise<VipSubscription> {
const client = await this.pool.connect();
try {
await setTenantContext(client, tenantId);
const result = await client.query(
`UPDATE vip.subscriptions SET status = $1 WHERE id = $2 RETURNING *`,
[status, subscriptionId]
);
if (result.rows.length === 0) {
throw new Error('Subscription not found');
}
return mapRowToSubscription(result.rows[0]);
} finally {
client.release();
}
}
async renewPeriod(
subscriptionId: string,
tenantId: string,
interval: 'month' | 'year' = 'month'
): Promise<VipSubscription> {
const client = await this.pool.connect();
try {
await setTenantContext(client, tenantId);
const now = new Date();
const periodEnd = new Date(
now.getTime() + (interval === 'year' ? 365 : 30) * 24 * 60 * 60 * 1000
);
const result = await client.query(
`UPDATE vip.subscriptions
SET status = 'active',
current_period_start = NOW(),
current_period_end = $1,
vip_predictions_used = 0,
api_calls_this_period = 0,
last_usage_reset = NOW()
WHERE id = $2
RETURNING *`,
[periodEnd, subscriptionId]
);
if (result.rows.length === 0) {
throw new Error('Subscription not found');
}
// Reset model access usage
await client.query(
`UPDATE vip.model_access
SET daily_used = 0, monthly_used = 0
WHERE subscription_id = $1`,
[subscriptionId]
);
return mapRowToSubscription(result.rows[0]);
} finally {
client.release();
}
}
}
let vipServiceInstance: VipService | null = null;
export function getVipService(): VipService {
if (!vipServiceInstance) {
vipServiceInstance = new VipService();
}
return vipServiceInstance;
}

66
src/tools/index.ts Normal file
View File

@ -0,0 +1,66 @@
export {
vip_get_tiers,
vip_get_tier,
vip_create_subscription,
vip_get_subscription,
vip_cancel_subscription,
vip_reactivate_subscription,
vip_upgrade_tier,
vip_check_model_access,
vip_get_model_access,
vip_record_model_usage,
vip_get_usage_stats,
vip_update_status,
vip_renew_period,
handleVipGetTiers,
handleVipGetTier,
handleVipCreateSubscription,
handleVipGetSubscription,
handleVipCancelSubscription,
handleVipReactivateSubscription,
handleVipUpgradeTier,
handleVipCheckModelAccess,
handleVipGetModelAccess,
handleVipRecordModelUsage,
handleVipGetUsageStats,
handleVipUpdateStatus,
handleVipRenewPeriod,
vipToolSchemas,
} from './vip';
import { vipToolSchemas } from './vip';
import {
handleVipGetTiers,
handleVipGetTier,
handleVipCreateSubscription,
handleVipGetSubscription,
handleVipCancelSubscription,
handleVipReactivateSubscription,
handleVipUpgradeTier,
handleVipCheckModelAccess,
handleVipGetModelAccess,
handleVipRecordModelUsage,
handleVipGetUsageStats,
handleVipUpdateStatus,
handleVipRenewPeriod,
} from './vip';
export const allToolSchemas = vipToolSchemas;
export const toolHandlers: Record<string, (params: unknown) => Promise<{ content: Array<{ type: string; text: string }> }>> = {
vip_get_tiers: handleVipGetTiers,
vip_get_tier: handleVipGetTier,
vip_create_subscription: handleVipCreateSubscription,
vip_get_subscription: handleVipGetSubscription,
vip_cancel_subscription: handleVipCancelSubscription,
vip_reactivate_subscription: handleVipReactivateSubscription,
vip_upgrade_tier: handleVipUpgradeTier,
vip_check_model_access: handleVipCheckModelAccess,
vip_get_model_access: handleVipGetModelAccess,
vip_record_model_usage: handleVipRecordModelUsage,
vip_get_usage_stats: handleVipGetUsageStats,
vip_update_status: handleVipUpdateStatus,
vip_renew_period: handleVipRenewPeriod,
};
export const toolNames = Object.keys(allToolSchemas);

511
src/tools/vip.ts Normal file
View File

@ -0,0 +1,511 @@
import { z } from 'zod';
import { getVipService } from '../services/vip.service';
import { logger } from '../utils/logger';
import {
VipTier,
VipSubscription,
SubscriptionWithTier,
ModelAccess,
UsageStats,
VipTierType,
SubscriptionStatus,
} from '../types/vip.types';
// ============================================================================
// INPUT SCHEMAS
// ============================================================================
export const GetTiersInputSchema = z.object({});
export const GetTierInputSchema = z.object({
tierId: z.string().uuid().optional(),
tier: z.enum(['GOLD', 'PLATINUM', 'DIAMOND'] as const).optional(),
}).refine(data => data.tierId || data.tier, { message: 'Either tierId or tier required' });
export const CreateSubscriptionInputSchema = z.object({
tenantId: z.string().uuid(),
userId: z.string().uuid(),
tier: z.enum(['GOLD', 'PLATINUM', 'DIAMOND'] as const),
interval: z.enum(['month', 'year'] as const).default('month'),
stripeSubscriptionId: z.string().optional(),
stripeCustomerId: z.string().optional(),
startTrial: z.boolean().default(false),
});
export const GetSubscriptionInputSchema = z.object({
tenantId: z.string().uuid(),
userId: z.string().uuid().optional(),
subscriptionId: z.string().uuid().optional(),
}).refine(data => data.userId || data.subscriptionId, { message: 'Either userId or subscriptionId required' });
export const CancelSubscriptionInputSchema = z.object({
tenantId: z.string().uuid(),
subscriptionId: z.string().uuid(),
reason: z.string().optional(),
cancelImmediately: z.boolean().default(false),
});
export const ReactivateSubscriptionInputSchema = z.object({
tenantId: z.string().uuid(),
subscriptionId: z.string().uuid(),
});
export const UpgradeTierInputSchema = z.object({
tenantId: z.string().uuid(),
subscriptionId: z.string().uuid(),
newTier: z.enum(['GOLD', 'PLATINUM', 'DIAMOND'] as const),
});
export const CheckModelAccessInputSchema = z.object({
tenantId: z.string().uuid(),
userId: z.string().uuid(),
modelId: z.string(),
});
export const GetModelAccessInputSchema = z.object({
tenantId: z.string().uuid(),
subscriptionId: z.string().uuid(),
});
export const RecordModelUsageInputSchema = z.object({
tenantId: z.string().uuid(),
subscriptionId: z.string().uuid(),
modelId: z.string(),
});
export const GetUsageStatsInputSchema = z.object({
tenantId: z.string().uuid(),
subscriptionId: z.string().uuid(),
});
export const UpdateStatusInputSchema = z.object({
tenantId: z.string().uuid(),
subscriptionId: z.string().uuid(),
status: z.enum(['trialing', 'active', 'past_due', 'cancelled', 'expired'] as const),
});
export const RenewPeriodInputSchema = z.object({
tenantId: z.string().uuid(),
subscriptionId: z.string().uuid(),
interval: z.enum(['month', 'year'] as const).default('month'),
});
// ============================================================================
// RESULT TYPE
// ============================================================================
interface ToolResult<T = unknown> {
success: boolean;
data?: T;
error?: string;
}
// ============================================================================
// TOOL IMPLEMENTATIONS
// ============================================================================
export async function vip_get_tiers(): Promise<ToolResult<VipTier[]>> {
try {
const service = getVipService();
const tiers = await service.getAllTiers();
return { success: true, data: tiers };
} catch (error) {
logger.error('vip_get_tiers failed', { error });
return { success: false, error: (error as Error).message };
}
}
export async function vip_get_tier(params: z.infer<typeof GetTierInputSchema>): Promise<ToolResult<VipTier>> {
try {
const service = getVipService();
let tier: VipTier | null = null;
if (params.tierId) {
tier = await service.getTierById(params.tierId);
} else if (params.tier) {
tier = await service.getTierByType(params.tier);
}
if (!tier) return { success: false, error: 'Tier not found' };
return { success: true, data: tier };
} catch (error) {
logger.error('vip_get_tier failed', { error, params });
return { success: false, error: (error as Error).message };
}
}
export async function vip_create_subscription(
params: z.infer<typeof CreateSubscriptionInputSchema>
): Promise<ToolResult<SubscriptionWithTier>> {
try {
const service = getVipService();
const subscription = await service.createSubscription(params);
return { success: true, data: subscription };
} catch (error) {
logger.error('vip_create_subscription failed', { error, params });
return { success: false, error: (error as Error).message };
}
}
export async function vip_get_subscription(
params: z.infer<typeof GetSubscriptionInputSchema>
): Promise<ToolResult<SubscriptionWithTier>> {
try {
const service = getVipService();
let subscription: SubscriptionWithTier | null = null;
if (params.subscriptionId) {
subscription = await service.getSubscriptionById(params.subscriptionId, params.tenantId);
} else if (params.userId) {
subscription = await service.getSubscription(params.userId, params.tenantId);
}
if (!subscription) return { success: false, error: 'Subscription not found' };
return { success: true, data: subscription };
} catch (error) {
logger.error('vip_get_subscription failed', { error, params });
return { success: false, error: (error as Error).message };
}
}
export async function vip_cancel_subscription(
params: z.infer<typeof CancelSubscriptionInputSchema>
): Promise<ToolResult<VipSubscription>> {
try {
const service = getVipService();
const subscription = await service.cancelSubscription(
params.subscriptionId,
params.tenantId,
params.reason,
params.cancelImmediately
);
return { success: true, data: subscription };
} catch (error) {
logger.error('vip_cancel_subscription failed', { error, params });
return { success: false, error: (error as Error).message };
}
}
export async function vip_reactivate_subscription(
params: z.infer<typeof ReactivateSubscriptionInputSchema>
): Promise<ToolResult<VipSubscription>> {
try {
const service = getVipService();
const subscription = await service.reactivateSubscription(params.subscriptionId, params.tenantId);
return { success: true, data: subscription };
} catch (error) {
logger.error('vip_reactivate_subscription failed', { error, params });
return { success: false, error: (error as Error).message };
}
}
export async function vip_upgrade_tier(
params: z.infer<typeof UpgradeTierInputSchema>
): Promise<ToolResult<SubscriptionWithTier>> {
try {
const service = getVipService();
const subscription = await service.upgradeTier(
params.subscriptionId,
params.newTier,
params.tenantId
);
return { success: true, data: subscription };
} catch (error) {
logger.error('vip_upgrade_tier failed', { error, params });
return { success: false, error: (error as Error).message };
}
}
export async function vip_check_model_access(
params: z.infer<typeof CheckModelAccessInputSchema>
): Promise<ToolResult<{ hasAccess: boolean }>> {
try {
const service = getVipService();
const hasAccess = await service.checkModelAccess(params.userId, params.modelId, params.tenantId);
return { success: true, data: { hasAccess } };
} catch (error) {
logger.error('vip_check_model_access failed', { error, params });
return { success: false, error: (error as Error).message };
}
}
export async function vip_get_model_access(
params: z.infer<typeof GetModelAccessInputSchema>
): Promise<ToolResult<ModelAccess[]>> {
try {
const service = getVipService();
const access = await service.getModelAccess(params.subscriptionId, params.tenantId);
return { success: true, data: access };
} catch (error) {
logger.error('vip_get_model_access failed', { error, params });
return { success: false, error: (error as Error).message };
}
}
export async function vip_record_model_usage(
params: z.infer<typeof RecordModelUsageInputSchema>
): Promise<ToolResult<ModelAccess>> {
try {
const service = getVipService();
const access = await service.recordModelUsage(
params.subscriptionId,
params.modelId,
params.tenantId
);
return { success: true, data: access };
} catch (error) {
logger.error('vip_record_model_usage failed', { error, params });
return { success: false, error: (error as Error).message };
}
}
export async function vip_get_usage_stats(
params: z.infer<typeof GetUsageStatsInputSchema>
): Promise<ToolResult<UsageStats>> {
try {
const service = getVipService();
const stats = await service.getUsageStats(params.subscriptionId, params.tenantId);
if (!stats) return { success: false, error: 'Subscription not found' };
return { success: true, data: stats };
} catch (error) {
logger.error('vip_get_usage_stats failed', { error, params });
return { success: false, error: (error as Error).message };
}
}
export async function vip_update_status(
params: z.infer<typeof UpdateStatusInputSchema>
): Promise<ToolResult<VipSubscription>> {
try {
const service = getVipService();
const subscription = await service.updateStatus(
params.subscriptionId,
params.status,
params.tenantId
);
return { success: true, data: subscription };
} catch (error) {
logger.error('vip_update_status failed', { error, params });
return { success: false, error: (error as Error).message };
}
}
export async function vip_renew_period(
params: z.infer<typeof RenewPeriodInputSchema>
): Promise<ToolResult<VipSubscription>> {
try {
const service = getVipService();
const subscription = await service.renewPeriod(
params.subscriptionId,
params.tenantId,
params.interval
);
return { success: true, data: subscription };
} catch (error) {
logger.error('vip_renew_period failed', { error, params });
return { success: false, error: (error as Error).message };
}
}
// ============================================================================
// MCP TOOL SCHEMAS
// ============================================================================
export const vipToolSchemas = {
vip_get_tiers: {
name: 'vip_get_tiers',
description: 'Get all available VIP tiers (Gold, Platinum, Diamond)',
inputSchema: { type: 'object', properties: {}, required: [] },
riskLevel: 'LOW',
},
vip_get_tier: {
name: 'vip_get_tier',
description: 'Get a specific VIP tier by ID or type',
inputSchema: {
type: 'object',
properties: {
tierId: { type: 'string' },
tier: { type: 'string', enum: ['GOLD', 'PLATINUM', 'DIAMOND'] },
},
},
riskLevel: 'LOW',
},
vip_create_subscription: {
name: 'vip_create_subscription',
description: 'Create a new VIP subscription for a user',
inputSchema: {
type: 'object',
properties: {
tenantId: { type: 'string' },
userId: { type: 'string' },
tier: { type: 'string', enum: ['GOLD', 'PLATINUM', 'DIAMOND'] },
interval: { type: 'string', enum: ['month', 'year'] },
stripeSubscriptionId: { type: 'string' },
stripeCustomerId: { type: 'string' },
startTrial: { type: 'boolean' },
},
required: ['tenantId', 'userId', 'tier'],
},
riskLevel: 'HIGH',
},
vip_get_subscription: {
name: 'vip_get_subscription',
description: 'Get user VIP subscription by user ID or subscription ID',
inputSchema: {
type: 'object',
properties: {
tenantId: { type: 'string' },
userId: { type: 'string' },
subscriptionId: { type: 'string' },
},
required: ['tenantId'],
},
riskLevel: 'LOW',
},
vip_cancel_subscription: {
name: 'vip_cancel_subscription',
description: 'Cancel a VIP subscription',
inputSchema: {
type: 'object',
properties: {
tenantId: { type: 'string' },
subscriptionId: { type: 'string' },
reason: { type: 'string' },
cancelImmediately: { type: 'boolean' },
},
required: ['tenantId', 'subscriptionId'],
},
riskLevel: 'HIGH',
},
vip_reactivate_subscription: {
name: 'vip_reactivate_subscription',
description: 'Reactivate a cancelled VIP subscription',
inputSchema: {
type: 'object',
properties: {
tenantId: { type: 'string' },
subscriptionId: { type: 'string' },
},
required: ['tenantId', 'subscriptionId'],
},
riskLevel: 'HIGH',
},
vip_upgrade_tier: {
name: 'vip_upgrade_tier',
description: 'Upgrade subscription to a higher tier',
inputSchema: {
type: 'object',
properties: {
tenantId: { type: 'string' },
subscriptionId: { type: 'string' },
newTier: { type: 'string', enum: ['GOLD', 'PLATINUM', 'DIAMOND'] },
},
required: ['tenantId', 'subscriptionId', 'newTier'],
},
riskLevel: 'HIGH',
},
vip_check_model_access: {
name: 'vip_check_model_access',
description: 'Check if user has VIP access to a specific ML model',
inputSchema: {
type: 'object',
properties: {
tenantId: { type: 'string' },
userId: { type: 'string' },
modelId: { type: 'string' },
},
required: ['tenantId', 'userId', 'modelId'],
},
riskLevel: 'LOW',
},
vip_get_model_access: {
name: 'vip_get_model_access',
description: 'Get all model access records for a subscription',
inputSchema: {
type: 'object',
properties: {
tenantId: { type: 'string' },
subscriptionId: { type: 'string' },
},
required: ['tenantId', 'subscriptionId'],
},
riskLevel: 'LOW',
},
vip_record_model_usage: {
name: 'vip_record_model_usage',
description: 'Record model usage for a VIP subscription',
inputSchema: {
type: 'object',
properties: {
tenantId: { type: 'string' },
subscriptionId: { type: 'string' },
modelId: { type: 'string' },
},
required: ['tenantId', 'subscriptionId', 'modelId'],
},
riskLevel: 'MEDIUM',
},
vip_get_usage_stats: {
name: 'vip_get_usage_stats',
description: 'Get usage statistics for a VIP subscription',
inputSchema: {
type: 'object',
properties: {
tenantId: { type: 'string' },
subscriptionId: { type: 'string' },
},
required: ['tenantId', 'subscriptionId'],
},
riskLevel: 'LOW',
},
vip_update_status: {
name: 'vip_update_status',
description: 'Update subscription status',
inputSchema: {
type: 'object',
properties: {
tenantId: { type: 'string' },
subscriptionId: { type: 'string' },
status: { type: 'string', enum: ['trialing', 'active', 'past_due', 'cancelled', 'expired'] },
},
required: ['tenantId', 'subscriptionId', 'status'],
},
riskLevel: 'HIGH',
},
vip_renew_period: {
name: 'vip_renew_period',
description: 'Renew subscription period and reset usage',
inputSchema: {
type: 'object',
properties: {
tenantId: { type: 'string' },
subscriptionId: { type: 'string' },
interval: { type: 'string', enum: ['month', 'year'] },
},
required: ['tenantId', 'subscriptionId'],
},
riskLevel: 'HIGH',
},
};
// ============================================================================
// MCP HANDLERS
// ============================================================================
function formatMcpResponse(result: ToolResult): { content: Array<{ type: string; text: string }> } {
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
}
export const handleVipGetTiers = async () => formatMcpResponse(await vip_get_tiers());
export const handleVipGetTier = async (p: unknown) => formatMcpResponse(await vip_get_tier(GetTierInputSchema.parse(p)));
export const handleVipCreateSubscription = async (p: unknown) => formatMcpResponse(await vip_create_subscription(CreateSubscriptionInputSchema.parse(p)));
export const handleVipGetSubscription = async (p: unknown) => formatMcpResponse(await vip_get_subscription(GetSubscriptionInputSchema.parse(p)));
export const handleVipCancelSubscription = async (p: unknown) => formatMcpResponse(await vip_cancel_subscription(CancelSubscriptionInputSchema.parse(p)));
export const handleVipReactivateSubscription = async (p: unknown) => formatMcpResponse(await vip_reactivate_subscription(ReactivateSubscriptionInputSchema.parse(p)));
export const handleVipUpgradeTier = async (p: unknown) => formatMcpResponse(await vip_upgrade_tier(UpgradeTierInputSchema.parse(p)));
export const handleVipCheckModelAccess = async (p: unknown) => formatMcpResponse(await vip_check_model_access(CheckModelAccessInputSchema.parse(p)));
export const handleVipGetModelAccess = async (p: unknown) => formatMcpResponse(await vip_get_model_access(GetModelAccessInputSchema.parse(p)));
export const handleVipRecordModelUsage = async (p: unknown) => formatMcpResponse(await vip_record_model_usage(RecordModelUsageInputSchema.parse(p)));
export const handleVipGetUsageStats = async (p: unknown) => formatMcpResponse(await vip_get_usage_stats(GetUsageStatsInputSchema.parse(p)));
export const handleVipUpdateStatus = async (p: unknown) => formatMcpResponse(await vip_update_status(UpdateStatusInputSchema.parse(p)));
export const handleVipRenewPeriod = async (p: unknown) => formatMcpResponse(await vip_renew_period(RenewPeriodInputSchema.parse(p)));

178
src/types/vip.types.ts Normal file
View File

@ -0,0 +1,178 @@
import Decimal from 'decimal.js';
export type VipTierType = 'GOLD' | 'PLATINUM' | 'DIAMOND';
export type SubscriptionStatus = 'trialing' | 'active' | 'past_due' | 'cancelled' | 'expired';
export interface TierBenefit {
name: string;
included?: boolean;
value?: string | number;
}
export interface TierLimits {
vip_predictions_per_month: number;
api_calls_per_month: number;
exclusive_models: string[];
priority_support?: boolean;
custom_alerts?: number;
[key: string]: unknown;
}
export interface VipTier {
id: string;
tier: VipTierType;
name: string;
description: string | null;
tagline: string | null;
priceMonthly: Decimal;
priceYearly: Decimal | null;
currency: string;
stripeProductId: string | null;
stripePriceMonthlyId: string | null;
stripePriceYearlyId: string | null;
benefits: TierBenefit[];
limits: TierLimits;
includedModels: string[];
color: string | null;
icon: string | null;
sortOrder: number;
isPopular: boolean;
isActive: boolean;
createdAt: Date;
updatedAt: Date;
}
export interface VipSubscription {
id: string;
tenantId: string;
userId: string;
tierId: string;
stripeSubscriptionId: string | null;
stripeCustomerId: string | null;
status: SubscriptionStatus;
interval: string;
currentPeriodStart: Date | null;
currentPeriodEnd: Date | null;
trialStart: Date | null;
trialEnd: Date | null;
cancelAt: Date | null;
cancelledAt: Date | null;
cancelReason: string | null;
vipPredictionsUsed: number;
apiCallsThisPeriod: number;
lastUsageReset: Date;
metadata: Record<string, unknown>;
createdAt: Date;
updatedAt: Date;
}
export interface SubscriptionWithTier extends VipSubscription {
tier: VipTier;
}
export interface ModelAccess {
id: string;
subscriptionId: string;
modelId: string;
modelName: string | null;
predictionsGenerated: number;
lastUsedAt: Date | null;
dailyLimit: number | null;
monthlyLimit: number | null;
dailyUsed: number;
monthlyUsed: number;
grantedAt: Date;
expiresAt: Date | null;
}
export interface CreateSubscriptionInput {
tenantId: string;
userId: string;
tier: VipTierType;
interval?: 'month' | 'year';
stripeSubscriptionId?: string;
stripeCustomerId?: string;
startTrial?: boolean;
}
export interface UsageStats {
predictionsUsed: number;
predictionsLimit: number;
predictionsRemaining: number;
apiCallsUsed: number;
apiCallsLimit: number;
apiCallsRemaining: number;
periodStart: Date;
periodEnd: Date;
daysRemaining: number;
}
export function mapRowToTier(row: Record<string, unknown>): VipTier {
return {
id: row.id as string,
tier: row.tier as VipTierType,
name: row.name as string,
description: row.description as string | null,
tagline: row.tagline as string | null,
priceMonthly: new Decimal(row.price_monthly as string),
priceYearly: row.price_yearly ? new Decimal(row.price_yearly as string) : null,
currency: row.currency as string,
stripeProductId: row.stripe_product_id as string | null,
stripePriceMonthlyId: row.stripe_price_monthly_id as string | null,
stripePriceYearlyId: row.stripe_price_yearly_id as string | null,
benefits: (row.benefits as TierBenefit[]) || [],
limits: (row.limits as TierLimits) || { vip_predictions_per_month: 0, api_calls_per_month: 0, exclusive_models: [] },
includedModels: (row.included_models as string[]) || [],
color: row.color as string | null,
icon: row.icon as string | null,
sortOrder: row.sort_order as number,
isPopular: row.is_popular as boolean,
isActive: row.is_active as boolean,
createdAt: new Date(row.created_at as string),
updatedAt: new Date(row.updated_at as string),
};
}
export function mapRowToSubscription(row: Record<string, unknown>): VipSubscription {
return {
id: row.id as string,
tenantId: row.tenant_id as string,
userId: row.user_id as string,
tierId: row.tier_id as string,
stripeSubscriptionId: row.stripe_subscription_id as string | null,
stripeCustomerId: row.stripe_customer_id as string | null,
status: row.status as SubscriptionStatus,
interval: row.interval as string,
currentPeriodStart: row.current_period_start ? new Date(row.current_period_start as string) : null,
currentPeriodEnd: row.current_period_end ? new Date(row.current_period_end as string) : null,
trialStart: row.trial_start ? new Date(row.trial_start as string) : null,
trialEnd: row.trial_end ? new Date(row.trial_end as string) : null,
cancelAt: row.cancel_at ? new Date(row.cancel_at as string) : null,
cancelledAt: row.cancelled_at ? new Date(row.cancelled_at as string) : null,
cancelReason: row.cancel_reason as string | null,
vipPredictionsUsed: row.vip_predictions_used as number,
apiCallsThisPeriod: row.api_calls_this_period as number,
lastUsageReset: new Date(row.last_usage_reset as string),
metadata: (row.metadata as Record<string, unknown>) || {},
createdAt: new Date(row.created_at as string),
updatedAt: new Date(row.updated_at as string),
};
}
export function mapRowToModelAccess(row: Record<string, unknown>): ModelAccess {
return {
id: row.id as string,
subscriptionId: row.subscription_id as string,
modelId: row.model_id as string,
modelName: row.model_name as string | null,
predictionsGenerated: row.predictions_generated as number,
lastUsedAt: row.last_used_at ? new Date(row.last_used_at as string) : null,
dailyLimit: row.daily_limit as number | null,
monthlyLimit: row.monthly_limit as number | null,
dailyUsed: row.daily_used as number,
monthlyUsed: row.monthly_used as number,
grantedAt: new Date(row.granted_at as string),
expiresAt: row.expires_at ? new Date(row.expires_at as string) : null,
};
}

28
src/utils/logger.ts Normal file
View File

@ -0,0 +1,28 @@
import winston from 'winston';
import { serverConfig } from '../config';
const { combine, timestamp, printf, colorize, errors } = winston.format;
const consoleFormat = printf(({ level, message, timestamp, ...meta }) => {
let msg = `${timestamp} [${level}]: ${message}`;
if (Object.keys(meta).length > 0) msg += ` ${JSON.stringify(meta)}`;
return msg;
});
export const logger = winston.createLogger({
level: serverConfig.logLevel,
format: combine(errors({ stack: true }), timestamp({ format: 'YYYY-MM-DD HH:mm:ss.SSS' })),
defaultMeta: { service: 'mcp-vip' },
transports: [
new winston.transports.Console({
format: combine(
serverConfig.nodeEnv === 'development' ? colorize() : winston.format.uncolorize(),
consoleFormat
),
}),
],
});
export function logError(context: string, error: Error, details: Record<string, unknown> = {}): void {
logger.error(`Error in ${context}`, { context, error: error.message, stack: error.stack, ...details });
}

19
tsconfig.json Normal file
View File

@ -0,0 +1,19 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "commonjs",
"lib": ["ES2022"],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"declaration": true,
"sourceMap": true,
"moduleResolution": "node"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}