mirror of
https://github.com/samanhappy/mcphub.git
synced 2025-12-30 21:49:13 -05:00
244 lines
7.5 KiB
TypeScript
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;
|