Files
mcphub/src/server.ts

244 lines
7.5 KiB
TypeScript

import express from 'express';
import cors from 'cors';
import config from './config/index.js';
import path from 'path';
import fs from 'fs';
import { initUpstreamServers, connected } from './services/mcpService.js';
import { initMiddlewares } from './middlewares/index.js';
import { initRoutes } from './routes/index.js';
import { initI18n } from './utils/i18n.js';
import {
handleSseConnection,
handleSseMessage,
handleMcpPostRequest,
handleMcpOtherRequest,
} from './services/sseService.js';
import { initializeDefaultUser } from './models/User.js';
import { sseUserContextMiddleware } from './middlewares/userContext.js';
import { findPackageRoot } from './utils/path.js';
import { getCurrentModuleDir } from './utils/moduleDir.js';
import { initOAuthProvider, getOAuthRouter } from './services/oauthService.js';
/**
* Get the directory of the current module
* This is wrapped in a function to allow easy mocking in test environments
*/
function getCurrentFileDir(): string {
// In test environments, use process.cwd() to avoid import.meta issues
if (process.env.NODE_ENV === 'test' || process.env.JEST_WORKER_ID !== undefined) {
return process.cwd();
}
try {
return getCurrentModuleDir();
} catch {
// Fallback for environments where import.meta might not be available
return process.cwd();
}
}
export class AppServer {
private app: express.Application;
private port: number | string;
private frontendPath: string | null = null;
private basePath: string;
constructor() {
this.app = express();
this.app.use(cors());
this.port = config.port;
this.basePath = config.basePath;
}
async initialize(): Promise<void> {
try {
// Initialize i18n before other components
await initI18n();
console.log('i18n initialized successfully');
// Initialize default admin user if no users exist
await initializeDefaultUser();
// Initialize OAuth provider if configured
initOAuthProvider();
const oauthRouter = getOAuthRouter();
if (oauthRouter) {
// Mount OAuth router at the root level (before other routes)
// This must be at root level as per MCP OAuth specification
this.app.use(oauthRouter);
console.log('OAuth router mounted successfully');
}
initMiddlewares(this.app);
initRoutes(this.app);
console.log('Server initialized successfully');
initUpstreamServers()
.then(() => {
console.log('MCP server initialized successfully');
// Original routes (global and group-based)
this.app.get(`${this.basePath}/sse/:group(.*)?`, sseUserContextMiddleware, (req, res) =>
handleSseConnection(req, res),
);
this.app.post(`${this.basePath}/messages`, sseUserContextMiddleware, handleSseMessage);
this.app.post(
`${this.basePath}/mcp/:group(.*)?`,
sseUserContextMiddleware,
handleMcpPostRequest,
);
this.app.get(
`${this.basePath}/mcp/:group(.*)?`,
sseUserContextMiddleware,
handleMcpOtherRequest,
);
this.app.delete(
`${this.basePath}/mcp/:group(.*)?`,
sseUserContextMiddleware,
handleMcpOtherRequest,
);
// User-scoped routes with user context middleware
this.app.get(`${this.basePath}/:user/sse/:group(.*)?`, sseUserContextMiddleware, (req, res) =>
handleSseConnection(req, res),
);
this.app.post(
`${this.basePath}/:user/messages`,
sseUserContextMiddleware,
handleSseMessage,
);
this.app.post(
`${this.basePath}/:user/mcp/:group(.*)?`,
sseUserContextMiddleware,
handleMcpPostRequest,
);
this.app.get(
`${this.basePath}/:user/mcp/:group(.*)?`,
sseUserContextMiddleware,
handleMcpOtherRequest,
);
this.app.delete(
`${this.basePath}/:user/mcp/:group(.*)?`,
sseUserContextMiddleware,
handleMcpOtherRequest,
);
})
.catch((error) => {
console.error('Error initializing MCP server:', error);
throw error;
})
.finally(() => {
// Find and serve frontend
this.findAndServeFrontend();
});
} catch (error) {
console.error('Error initializing server:', error);
throw error;
}
}
private findAndServeFrontend(): void {
// Find frontend path
this.frontendPath = this.findFrontendDistPath();
if (this.frontendPath) {
console.log(`Serving frontend from: ${this.frontendPath}`);
// Serve static files with base path
this.app.use(this.basePath, express.static(this.frontendPath));
// Add the wildcard route for SPA with base path
if (fs.existsSync(path.join(this.frontendPath, 'index.html'))) {
this.app.get(`${this.basePath}/*`, (_req, res) => {
res.sendFile(path.join(this.frontendPath!, 'index.html'));
});
// Also handle root redirect if base path is set
if (this.basePath) {
this.app.get('/', (_req, res) => {
res.redirect(this.basePath);
});
}
}
} else {
console.warn('Frontend dist directory not found. Server will run without frontend.');
const rootPath = this.basePath || '/';
this.app.get(rootPath, (_req, res) => {
res
.status(404)
.send('Frontend not found. MCPHub API is running, but the UI is not available.');
});
}
}
start(): void {
this.app.listen(this.port, () => {
console.log(`Server is running on port ${this.port}`);
if (this.frontendPath) {
console.log(`Open http://localhost:${this.port} in your browser to access MCPHub UI`);
} else {
console.log(
`MCPHub API is running on http://localhost:${this.port}, but the UI is not available`,
);
}
});
}
connected(): boolean {
return connected();
}
getApp(): express.Application {
return this.app;
}
// Helper method to find frontend dist path in different environments
private findFrontendDistPath(): string | null {
// Debug flag for detailed logging
const debug = process.env.DEBUG === 'true';
const currentDir = getCurrentFileDir();
if (debug) {
console.log('DEBUG: Current directory:', process.cwd());
console.log('DEBUG: Script directory:', currentDir);
}
// First, find the package root directory
const packageRoot = this.findPackageRoot();
if (debug) {
console.log('DEBUG: Using package root:', packageRoot);
}
if (!packageRoot) {
console.warn('Could not determine package root directory');
return null;
}
// Check for frontend dist in the standard location
const frontendDistPath = path.join(packageRoot, 'frontend', 'dist');
if (debug) {
console.log(`DEBUG: Checking frontend at: ${frontendDistPath}`);
}
if (
fs.existsSync(frontendDistPath) &&
fs.existsSync(path.join(frontendDistPath, 'index.html'))
) {
return frontendDistPath;
}
console.warn('Frontend distribution not found at', frontendDistPath);
return null;
}
// Helper method to find the package root (where package.json is located)
private findPackageRoot(): string | null {
// Use the shared utility function which properly handles ESM module paths
const currentDir = getCurrentFileDir();
return findPackageRoot(currentDir);
}
}
export default AppServer;