feat: enhance configuration file handling and dynamic frontend path resolution (#40)

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
samanhappy
2025-04-30 22:38:21 +08:00
committed by GitHub
parent 7887a3a5f9
commit 0a6259decf
8 changed files with 400 additions and 43 deletions

96
bin/cli.js Executable file
View File

@@ -0,0 +1,96 @@
#!/usr/bin/env node
import path from 'path';
import { fileURLToPath } from 'url';
import { execSync } from 'child_process';
import fs from 'fs';
// Enable debug logging if needed
// process.env.DEBUG = 'true';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// Start with more debug information
console.log('📋 MCPHub CLI');
console.log(`📁 CLI script location: ${__dirname}`);
// The npm package directory structure when installed is:
// node_modules/@samanhappy/mcphub/
// - dist/
// - bin/
// - frontend/dist/
// Get the package root - this is where package.json is located
function findPackageRoot() {
const isDebug = process.env.DEBUG === 'true';
// Possible locations for package.json
const possibleRoots = [
// Standard npm package location
path.resolve(__dirname, '..'),
// When installed via npx
path.resolve(__dirname, '..', '..', '..')
];
// Special handling for npx
if (process.argv[1] && process.argv[1].includes('_npx')) {
const npxDir = path.dirname(process.argv[1]);
possibleRoots.unshift(path.resolve(npxDir, '..'));
}
if (isDebug) {
console.log('DEBUG: Checking for package.json in:', possibleRoots);
}
for (const root of possibleRoots) {
const packageJsonPath = path.join(root, 'package.json');
if (fs.existsSync(packageJsonPath)) {
try {
const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
if (pkg.name === 'mcphub' || pkg.name === '@samanhappy/mcphub') {
if (isDebug) {
console.log(`DEBUG: Found package.json at ${packageJsonPath}`);
}
return root;
}
} catch (e) {
// Continue to the next potential root
}
}
}
console.log('⚠️ Could not find package.json, using default path');
return path.resolve(__dirname, '..');
}
// Locate and check the frontend distribution
function checkFrontend(packageRoot) {
const isDebug = process.env.DEBUG === 'true';
const frontendDistPath = path.join(packageRoot, 'frontend', 'dist');
if (isDebug) {
console.log(`DEBUG: Checking frontend at: ${frontendDistPath}`);
}
if (fs.existsSync(frontendDistPath) && fs.existsSync(path.join(frontendDistPath, 'index.html'))) {
console.log('✅ Frontend distribution found');
return true;
} else {
console.log('⚠️ Frontend distribution not found at', frontendDistPath);
return false;
}
}
const projectRoot = findPackageRoot();
console.log(`📦 Using package root: ${projectRoot}`);
// Check if frontend exists
checkFrontend(projectRoot);
// Start the server
console.log('🚀 Starting MCPHub server...');
import(path.join(projectRoot, 'dist', 'index.js')).catch(err => {
console.error('Failed to start MCPHub:', err);
process.exit(1);
});

View File

@@ -1,11 +1,24 @@
{ {
"name": "mcphub", "name": "@samanhappy/mcphub",
"version": "0.0.1", "version": "0.0.27",
"description": "A hub server for mcp servers", "description": "A hub server for mcp servers",
"main": "dist/index.js", "main": "dist/index.js",
"type": "module", "type": "module",
"bin": {
"mcphub": "bin/cli.js"
},
"files": [
"dist",
"bin",
"mcp_settings.json",
"servers.json",
"frontend/dist",
"README.md",
"LICENSE"
],
"scripts": { "scripts": {
"build": "tsc", "build": "pnpm backend:build && pnpm frontend:build",
"backend:build": "tsc",
"start": "node dist/index.js", "start": "node dist/index.js",
"backend:dev": "tsx watch src/index.ts", "backend:dev": "tsx watch src/index.ts",
"lint": "eslint . --ext .ts", "lint": "eslint . --ext .ts",
@@ -14,36 +27,55 @@
"frontend:dev": "cd frontend && vite", "frontend:dev": "cd frontend && vite",
"frontend:build": "cd frontend && vite build", "frontend:build": "cd frontend && vite build",
"frontend:preview": "cd frontend && vite preview", "frontend:preview": "cd frontend && vite preview",
"dev": "concurrently \"pnpm backend:dev\" \"pnpm frontend:dev\"" "dev": "concurrently \"pnpm backend:dev\" \"pnpm frontend:dev\"",
"prepublishOnly": "npm run build && node scripts/verify-dist.js"
}, },
"keywords": [ "keywords": [
"typescript", "typescript",
"server" "server",
"mcp",
"model context protocol"
], ],
"author": "", "author": "",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@modelcontextprotocol/sdk": "^1.10.2", "@modelcontextprotocol/sdk": "^1.10.2",
"@radix-ui/react-accordion": "^1.2.3",
"@radix-ui/react-slot": "^1.1.2",
"@shadcn/ui": "^0.0.4",
"@tailwindcss/vite": "^4.1.3",
"@types/react": "^19.0.12",
"@types/react-dom": "^19.0.4",
"@types/uuid": "^10.0.0",
"autoprefixer": "^10.4.21",
"bcryptjs": "^3.0.2", "bcryptjs": "^3.0.2",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"dotenv": "^16.3.1", "dotenv": "^16.3.1",
"express": "^4.18.2", "express": "^4.18.2",
"express-validator": "^7.2.1", "express-validator": "^7.2.1",
"jsonwebtoken": "^9.0.2",
"uuid": "^11.1.0"
},
"devDependencies": {
"@radix-ui/react-accordion": "^1.2.3",
"@radix-ui/react-slot": "^1.1.2",
"@shadcn/ui": "^0.0.4",
"@tailwindcss/postcss": "^4.1.3",
"@tailwindcss/vite": "^4.1.3",
"@types/bcryptjs": "^3.0.0",
"@types/express": "^4.17.21",
"@types/jest": "^29.5.5",
"@types/jsonwebtoken": "^9.0.9",
"@types/node": "^20.8.2",
"@types/react": "^19.0.12",
"@types/react-dom": "^19.0.4",
"@types/uuid": "^10.0.0",
"@typescript-eslint/eslint-plugin": "^6.7.4",
"@typescript-eslint/parser": "^6.7.4",
"@vitejs/plugin-react": "^4.2.1",
"autoprefixer": "^10.4.21",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"concurrently": "^8.2.2",
"eslint": "^8.50.0",
"i18next": "^24.2.3", "i18next": "^24.2.3",
"i18next-browser-languagedetector": "^8.0.4", "i18next-browser-languagedetector": "^8.0.4",
"jsonwebtoken": "^9.0.2", "jest": "^29.7.0",
"lucide-react": "^0.486.0", "lucide-react": "^0.486.0",
"next": "^15.2.4", "next": "^15.2.4",
"postcss": "^8.5.3", "postcss": "^8.5.3",
"prettier": "^3.0.3",
"react": "^19.1.0", "react": "^19.1.0",
"react-dom": "^19.1.0", "react-dom": "^19.1.0",
"react-i18next": "^15.4.1", "react-i18next": "^15.4.1",
@@ -51,27 +83,14 @@
"tailwind-merge": "^3.1.0", "tailwind-merge": "^3.1.0",
"tailwind-scrollbar-hide": "^2.0.0", "tailwind-scrollbar-hide": "^2.0.0",
"tailwindcss": "^4.0.17", "tailwindcss": "^4.0.17",
"uuid": "^11.1.0",
"zod": "^3.24.2"
},
"devDependencies": {
"@tailwindcss/postcss": "^4.1.3",
"@types/bcryptjs": "^3.0.0",
"@types/express": "^4.17.21",
"@types/jest": "^29.5.5",
"@types/jsonwebtoken": "^9.0.9",
"@types/node": "^20.8.2",
"@typescript-eslint/eslint-plugin": "^6.7.4",
"@typescript-eslint/parser": "^6.7.4",
"@vitejs/plugin-react": "^4.2.1",
"concurrently": "^8.2.2",
"eslint": "^8.50.0",
"jest": "^29.7.0",
"prettier": "^3.0.3",
"ts-jest": "^29.1.1", "ts-jest": "^29.1.1",
"ts-node-dev": "^2.0.0", "ts-node-dev": "^2.0.0",
"tsx": "^4.7.0", "tsx": "^4.7.0",
"typescript": "^5.2.2", "typescript": "^5.2.2",
"vite": "^5.4.18" "vite": "^5.4.18",
"zod": "^3.24.2"
},
"engines": {
"node": ">=16.0.0"
} }
} }

44
scripts/verify-dist.js Executable file
View File

@@ -0,0 +1,44 @@
// scripts/verify-dist.js
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const projectRoot = path.resolve(__dirname, '..');
// Check if frontend dist exists
const frontendDistPath = path.join(projectRoot, 'frontend', 'dist');
const frontendIndexPath = path.join(frontendDistPath, 'index.html');
if (!fs.existsSync(frontendDistPath)) {
console.error('❌ Error: frontend/dist directory does not exist!');
console.error('Run "npm run frontend:build" to generate the frontend dist files.');
process.exit(1);
}
if (!fs.existsSync(frontendIndexPath)) {
console.error('❌ Error: frontend/dist/index.html does not exist!');
console.error('Frontend build may be incomplete. Run "npm run frontend:build" again.');
process.exit(1);
}
// Check if backend dist exists
const backendDistPath = path.join(projectRoot, 'dist');
const serverJsPath = path.join(backendDistPath, 'server.js');
if (!fs.existsSync(backendDistPath)) {
console.error('❌ Error: dist directory does not exist!');
console.error('Run "npm run backend:build" to generate the backend dist files.');
process.exit(1);
}
if (!fs.existsSync(serverJsPath)) {
console.error('❌ Error: dist/server.js does not exist!');
console.error('Backend build may be incomplete. Run "npm run backend:build" again.');
process.exit(1);
}
// All checks passed
console.log('✅ Verification passed! Frontend and backend dist files are present.');
console.log('📦 Package is ready for publishing.');

View File

@@ -1,7 +1,7 @@
import dotenv from 'dotenv'; import dotenv from 'dotenv';
import path from 'path';
import fs from 'fs'; import fs from 'fs';
import { McpSettings } from '../types/index.js'; import { McpSettings } from '../types/index.js';
import { getConfigFilePath } from '../utils/path.js';
dotenv.config(); dotenv.config();
@@ -14,7 +14,7 @@ const defaultConfig = {
}; };
export const getSettingsPath = (): string => { export const getSettingsPath = (): string => {
return path.resolve(process.cwd(), 'mcp_settings.json'); return getConfigFilePath('mcp_settings.json', 'Settings');
}; };
export const loadSettings = (): McpSettings => { export const loadSettings = (): McpSettings => {

View File

@@ -1,8 +1,36 @@
import express, { Request, Response, NextFunction } from 'express'; import express, { Request, Response, NextFunction } from 'express';
import path from 'path'; import path from 'path';
import { fileURLToPath } from 'url';
import { dirname } from 'path';
import fs from 'fs';
import { auth } from './auth.js'; import { auth } from './auth.js';
import { initializeDefaultUser } from '../models/User.js'; import { initializeDefaultUser } from '../models/User.js';
// Create __dirname equivalent for ES modules
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
// Try to find the correct frontend file path
const findFrontendPath = (): string => {
// First try development environment path
const devPath = path.join(dirname(__dirname), 'frontend', 'dist', 'index.html');
if (fs.existsSync(devPath)) {
return path.join(dirname(__dirname), 'frontend', 'dist');
}
// Try npm/npx installed path (remove /dist directory)
const npmPath = path.join(dirname(dirname(__dirname)), 'frontend', 'dist', 'index.html');
if (fs.existsSync(npmPath)) {
return path.join(dirname(dirname(__dirname)), 'frontend', 'dist');
}
// If none of the above paths exist, return the most reasonable default path and log a warning
console.warn('Warning: Could not locate frontend files. Using default path.');
return path.join(dirname(__dirname), 'frontend', 'dist');
};
const frontendPath = findFrontendPath();
export const errorHandler = ( export const errorHandler = (
err: Error, err: Error,
_req: Request, _req: Request,
@@ -17,7 +45,8 @@ export const errorHandler = (
}; };
export const initMiddlewares = (app: express.Application): void => { export const initMiddlewares = (app: express.Application): void => {
app.use(express.static('frontend/dist')); // Serve static files from the dynamically determined frontend path
app.use(express.static(frontendPath));
app.use((req, res, next) => { app.use((req, res, next) => {
if (req.path !== '/sse' && req.path !== '/messages') { if (req.path !== '/sse' && req.path !== '/messages') {
@@ -36,7 +65,8 @@ export const initMiddlewares = (app: express.Application): void => {
app.use('/api', auth); app.use('/api', auth);
app.get('/', (_req: Request, res: Response) => { app.get('/', (_req: Request, res: Response) => {
res.sendFile(path.join(process.cwd(), 'frontend', 'dist', 'index.html')); // Serve the frontend application
res.sendFile(path.join(frontendPath, 'index.html'));
}); });
app.use(errorHandler); app.use(errorHandler);

View File

@@ -1,6 +1,8 @@
import express from 'express'; import express from 'express';
import config from './config/index.js'; import config from './config/index.js';
import path from 'path'; import path from 'path';
import { fileURLToPath } from 'url';
import fs from 'fs';
import { initMcpServer } from './services/mcpService.js'; import { initMcpServer } from './services/mcpService.js';
import { initMiddlewares } from './middlewares/index.js'; import { initMiddlewares } from './middlewares/index.js';
import { initRoutes } from './routes/index.js'; import { initRoutes } from './routes/index.js';
@@ -13,9 +15,14 @@ import {
import { migrateUserData } from './utils/migration.js'; import { migrateUserData } from './utils/migration.js';
import { initializeDefaultUser } from './models/User.js'; import { initializeDefaultUser } from './models/User.js';
// Get the directory name in ESM
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
export class AppServer { export class AppServer {
private app: express.Application; private app: express.Application;
private port: number | string; private port: number | string;
private frontendPath: string | null = null;
constructor() { constructor() {
this.app = express(); this.app = express();
@@ -48,9 +55,8 @@ export class AppServer {
throw error; throw error;
}) })
.finally(() => { .finally(() => {
this.app.get('*', (_req, res) => { // Find and serve frontend
res.sendFile(path.join(process.cwd(), 'frontend', 'dist', 'index.html')); this.findAndServeFrontend();
});
}); });
} catch (error) { } catch (error) {
console.error('Error initializing server:', error); console.error('Error initializing server:', error);
@@ -58,15 +64,135 @@ export class AppServer {
} }
} }
private findAndServeFrontend(): void {
// Find frontend path
this.frontendPath = this.findFrontendDistPath();
if (this.frontendPath) {
console.log(`Serving frontend from: ${this.frontendPath}`);
this.app.use(express.static(this.frontendPath));
// Add the wildcard route for SPA
if (fs.existsSync(path.join(this.frontendPath, 'index.html'))) {
this.app.get('*', (_req, res) => {
res.sendFile(path.join(this.frontendPath!, 'index.html'));
});
}
} else {
console.warn('Frontend dist directory not found. Server will run without frontend.');
this.app.get('/', (_req, res) => {
res
.status(404)
.send('Frontend not found. MCPHub API is running, but the UI is not available.');
});
}
}
start(): void { start(): void {
this.app.listen(this.port, () => { this.app.listen(this.port, () => {
console.log(`Server is running on port ${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`,
);
}
}); });
} }
getApp(): express.Application { getApp(): express.Application {
return this.app; 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';
if (debug) {
console.log('DEBUG: Current directory:', process.cwd());
console.log('DEBUG: Script directory:', __dirname);
}
// 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 {
const debug = process.env.DEBUG === 'true';
// Possible locations for package.json
const possibleRoots = [
// Standard npm package location
path.resolve(__dirname, '..', '..'),
// Current working directory
process.cwd(),
// When running from dist directory
path.resolve(__dirname, '..'),
// When installed via npx
path.resolve(__dirname, '..', '..', '..'),
];
// Special handling for npx
if (process.argv[1] && process.argv[1].includes('_npx')) {
const npxDir = path.dirname(process.argv[1]);
possibleRoots.unshift(path.resolve(npxDir, '..'));
}
if (debug) {
console.log('DEBUG: Checking for package.json in:', possibleRoots);
}
for (const root of possibleRoots) {
const packageJsonPath = path.join(root, 'package.json');
if (fs.existsSync(packageJsonPath)) {
try {
const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
if (pkg.name === 'mcphub' || pkg.name === '@samanhappy/mcphub') {
if (debug) {
console.log(`DEBUG: Found package.json at ${packageJsonPath}`);
}
return root;
}
} catch (e) {
if (debug) {
console.error(`DEBUG: Failed to parse package.json at ${packageJsonPath}:`, e);
}
// Continue to the next potential root
}
}
}
return null;
}
} }
export default AppServer; export default AppServer;

View File

@@ -1,10 +1,10 @@
import fs from 'fs'; import fs from 'fs';
import path from 'path';
import { MarketServer } from '../types/index.js'; import { MarketServer } from '../types/index.js';
import { getConfigFilePath } from '../utils/path.js';
// Get path to the servers.json file // Get path to the servers.json file
export const getServersJsonPath = (): string => { export const getServersJsonPath = (): string => {
return path.resolve(process.cwd(), 'servers.json'); return getConfigFilePath('servers.json', 'Servers');
}; };
// Load all market servers from servers.json // Load all market servers from servers.json

42
src/utils/path.ts Normal file
View File

@@ -0,0 +1,42 @@
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import { dirname } from 'path';
// Get current file's directory
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
// Project root directory should be the parent directory of src
const rootDir = dirname(dirname(__dirname));
/**
* Find the path to a configuration file by checking multiple potential locations.
* @param filename The name of the file to locate (e.g., 'servers.json', 'mcp_settings.json')
* @param description Brief description of the file for logging purposes
* @returns The path to the file
*/
export const getConfigFilePath = (filename: string, description = 'Configuration'): string => {
// Try to find the correct path to the file
const potentialPaths = [
// Prioritize process.cwd() as the first location to check
path.resolve(process.cwd(), filename),
// Use path relative to the root directory
path.join(rootDir, filename),
// If installed with npx, may need to look one level up
path.join(dirname(rootDir), filename)
];
for (const filePath of potentialPaths) {
if (fs.existsSync(filePath)) {
return filePath;
}
}
// If all paths do not exist, use default path
// Using the default path is acceptable because it ensures the application can proceed
// even if the configuration file is missing. This fallback is particularly useful in
// development environments or when the file is optional.
const defaultPath = path.resolve(process.cwd(), filename);
console.debug(`${description} file not found at any expected location, using default path: ${defaultPath}`);
return defaultPath;
};