import express, { Application, Request, Response, NextFunction } from 'express';
import cors from 'cors';
import helmet from 'helmet';
import cookieParser from 'cookie-parser';
import rateLimit from 'express-rate-limit';
import { config } from '../config/environment';
import { logger } from '../utils/logger';
import { oauthManager } from '../auth/oauth';
import { requireAuth, optionalAuth, errorHandler, extractBearerToken } from '../auth/middleware';
import { createMCPRouter } from '../mcp/handler';
export function createApp(): Application {
const app = express();
// Trust proxy for rate limiting behind reverse proxy
app.set('trust proxy', 1);
// Security middleware
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", 'data:', 'https:'],
},
},
}));
// CORS configuration
const corsOptions: cors.CorsOptions = {
origin: config.CORS_ORIGINS === '*'
? true
: config.CORS_ORIGINS.split(',').map((o) => o.trim()),
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization', 'X-Session-ID'],
};
app.use(cors(corsOptions));
// Parse JSON and cookies
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true }));
app.use(cookieParser());
// Global rate limiting
const limiter = rateLimit({
windowMs: config.RATE_LIMIT_WINDOW_MS,
max: config.RATE_LIMIT_MAX_REQUESTS,
message: {
error: 'Too Many Requests',
message: 'Rate limit exceeded. Please try again later.',
},
standardHeaders: true,
legacyHeaders: false,
});
app.use(limiter);
// Request logging
app.use((req: Request, res: Response, next: NextFunction) => {
const start = Date.now();
res.on('finish', () => {
const duration = Date.now() - start;
logger.info(`${req.method} ${req.path} ${res.statusCode} ${duration}ms`);
});
next();
});
// Health check endpoint
app.get('/health', (req: Request, res: Response) => {
res.json({
status: 'healthy',
timestamp: new Date().toISOString(),
version: config.MCP_SERVER_VERSION,
});
});
// Server info endpoint
app.get('/', (req: Request, res: Response) => {
res.json({
name: config.MCP_SERVER_NAME,
version: config.MCP_SERVER_VERSION,
description: 'Tanda Workforce MCP Server with OAuth2 authentication',
endpoints: {
health: '/health',
auth: {
login: '/auth/login',
callback: '/auth/callback',
logout: '/auth/logout',
status: '/auth/status',
},
api: {
authenticate: '/api/authenticate',
},
mcp: '/mcp',
},
documentation: '/docs',
});
});
// ==================== OAuth Routes ====================
// GET /auth/login - Initiate OAuth flow
app.get('/auth/login', (req: Request, res: Response) => {
const { sessionId, authUrl } = oauthManager.createAuthSession();
// Set session cookie
res.cookie('tanda_session', sessionId, {
httpOnly: true,
secure: config.NODE_ENV === 'production',
sameSite: 'lax',
maxAge: 24 * 60 * 60 * 1000, // 24 hours
});
logger.info(`OAuth login initiated, redirecting to Tanda`);
res.redirect(authUrl);
});
// GET /auth/callback - Handle OAuth callback
app.get('/auth/callback', async (req: Request, res: Response) => {
const { code, state, error, error_description } = req.query;
if (error) {
logger.error(`OAuth callback error: ${error} - ${error_description}`);
res.status(400).json({
error: 'OAuth Error',
message: error_description || error,
});
return;
}
if (!code || !state) {
res.status(400).json({
error: 'Bad Request',
message: 'Missing code or state parameter',
});
return;
}
const sessionId = req.cookies.tanda_session;
if (!sessionId) {
res.status(400).json({
error: 'Bad Request',
message: 'No session cookie found. Please start the login flow again.',
});
return;
}
const result = await oauthManager.handleCallback(
sessionId,
code as string,
state as string
);
if (!result.success) {
res.status(400).json({
error: 'Authentication Failed',
message: result.error,
});
return;
}
// For browser-based flows, you might redirect to a frontend page
// For API clients, return the token directly
res.json({
success: true,
token: result.token,
user: {
id: result.user?.id,
name: result.user?.name,
email: result.user?.email,
},
message: 'Authentication successful. Use the token in the Authorization header for API requests.',
});
});
// POST /api/authenticate - Exchange code for token (API version)
app.post('/api/authenticate', async (req: Request, res: Response) => {
const { code } = req.body;
if (!code) {
res.status(400).json({
error: 'Bad Request',
message: 'Authorization code is required',
});
return;
}
const result = await oauthManager.exchangeCodeForToken(code);
if (!result.success) {
res.status(400).json({
error: 'Authentication Failed',
message: result.error,
});
return;
}
res.json({
success: true,
access_token: result.data?.access_token,
token_type: result.data?.token_type,
expires_in: result.data?.expires_in,
jwt: result.data?.jwt,
user: result.data?.user,
});
});
// GET /auth/status - Check authentication status
app.get('/auth/status', (req: Request, res: Response) => {
const token = extractBearerToken(req);
if (!token) {
res.json({
authenticated: false,
message: 'No token provided',
});
return;
}
const payload = oauthManager.verifyJWT(token);
if (!payload) {
res.json({
authenticated: false,
message: 'Invalid or expired token',
});
return;
}
const session = oauthManager.getSession(payload.sessionId);
res.json({
authenticated: true,
userId: payload.userId,
email: payload.email,
sessionActive: !!session,
user: session?.user ? {
id: session.user.id,
name: session.user.name,
email: session.user.email,
} : null,
});
});
// POST /auth/logout - Logout and invalidate session
app.post('/auth/logout', (req: Request, res: Response) => {
const token = extractBearerToken(req);
const sessionId = req.cookies.tanda_session;
let invalidated = false;
if (token) {
const payload = oauthManager.verifyJWT(token);
if (payload) {
invalidated = oauthManager.invalidateSession(payload.sessionId);
}
}
if (sessionId && !invalidated) {
invalidated = oauthManager.invalidateSession(sessionId);
}
res.clearCookie('tanda_session');
res.json({
success: true,
message: invalidated ? 'Logged out successfully' : 'No active session found',
});
});
// ==================== MCP Endpoint ====================
// POST /mcp - MCP protocol endpoint (authentication optional for initialize/list)
app.post('/mcp', optionalAuth, createMCPRouter());
// ==================== Protected API Routes ====================
// GET /api/me - Get current user (protected)
app.get('/api/me', requireAuth, async (req: Request, res: Response) => {
try {
const user = await req.auth!.tandaClient.getCurrentUser();
res.json(user);
} catch (error) {
res.status(500).json({
error: 'API Error',
message: error instanceof Error ? error.message : 'Failed to fetch user',
});
}
});
// Monitoring endpoint
app.get('/stats', (req: Request, res: Response) => {
const stats = oauthManager.getStats();
res.json({
server: {
name: config.MCP_SERVER_NAME,
version: config.MCP_SERVER_VERSION,
uptime: process.uptime(),
memory: process.memoryUsage(),
},
sessions: stats,
});
});
// Documentation endpoint
app.get('/docs', (req: Request, res: Response) => {
res.json({
title: 'Tanda Workforce MCP Server API',
version: config.MCP_SERVER_VERSION,
authentication: {
description: 'This server uses OAuth2 for authentication with Tanda',
flows: {
browserFlow: {
step1: 'Navigate to /auth/login to start OAuth flow',
step2: 'User authenticates with Tanda',
step3: 'Callback returns JWT token',
step4: 'Use JWT in Authorization header for subsequent requests',
},
apiFlow: {
step1: 'Redirect user to Tanda OAuth URL with your client_id',
step2: 'User authenticates and is redirected back with code',
step3: 'POST code to /api/authenticate',
step4: 'Use returned JWT in Authorization header',
},
},
headers: {
Authorization: 'Bearer <jwt_token>',
},
},
endpoints: {
'/': { method: 'GET', description: 'Server info', auth: false },
'/health': { method: 'GET', description: 'Health check', auth: false },
'/auth/login': { method: 'GET', description: 'Start OAuth flow', auth: false },
'/auth/callback': { method: 'GET', description: 'OAuth callback', auth: false },
'/auth/status': { method: 'GET', description: 'Check auth status', auth: false },
'/auth/logout': { method: 'POST', description: 'Logout', auth: false },
'/api/authenticate': { method: 'POST', description: 'Exchange code for token', auth: false },
'/api/me': { method: 'GET', description: 'Get current user', auth: true },
'/mcp': { method: 'POST', description: 'MCP protocol endpoint', auth: 'optional' },
'/stats': { method: 'GET', description: 'Server statistics', auth: false },
},
mcp: {
description: 'MCP (Model Context Protocol) endpoint for AI integrations',
protocol: 'JSON-RPC 2.0',
methods: [
'initialize',
'tools/list',
'tools/call',
'resources/list',
'resources/read',
'prompts/list',
'prompts/get',
'ping',
],
},
});
});
// 404 handler
app.use((req: Request, res: Response) => {
res.status(404).json({
error: 'Not Found',
message: `Cannot ${req.method} ${req.path}`,
});
});
// Error handler
app.use(errorHandler);
return app;
}