diff --git a/bin/cli.js b/bin/cli.js new file mode 100755 index 0000000..cf704d9 --- /dev/null +++ b/bin/cli.js @@ -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); +}); \ No newline at end of file diff --git a/package.json b/package.json index f9cc78c..64b06ea 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,24 @@ { - "name": "mcphub", - "version": "0.0.1", + "name": "@samanhappy/mcphub", + "version": "0.0.27", "description": "A hub server for mcp servers", "main": "dist/index.js", "type": "module", + "bin": { + "mcphub": "bin/cli.js" + }, + "files": [ + "dist", + "bin", + "mcp_settings.json", + "servers.json", + "frontend/dist", + "README.md", + "LICENSE" + ], "scripts": { - "build": "tsc", + "build": "pnpm backend:build && pnpm frontend:build", + "backend:build": "tsc", "start": "node dist/index.js", "backend:dev": "tsx watch src/index.ts", "lint": "eslint . --ext .ts", @@ -14,36 +27,55 @@ "frontend:dev": "cd frontend && vite", "frontend:build": "cd frontend && vite build", "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": [ "typescript", - "server" + "server", + "mcp", + "model context protocol" ], "author": "", "license": "ISC", "dependencies": { "@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", - "class-variance-authority": "^0.7.1", - "clsx": "^2.1.1", "dotenv": "^16.3.1", "express": "^4.18.2", "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-browser-languagedetector": "^8.0.4", - "jsonwebtoken": "^9.0.2", + "jest": "^29.7.0", "lucide-react": "^0.486.0", "next": "^15.2.4", "postcss": "^8.5.3", + "prettier": "^3.0.3", "react": "^19.1.0", "react-dom": "^19.1.0", "react-i18next": "^15.4.1", @@ -51,27 +83,14 @@ "tailwind-merge": "^3.1.0", "tailwind-scrollbar-hide": "^2.0.0", "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-node-dev": "^2.0.0", "tsx": "^4.7.0", "typescript": "^5.2.2", - "vite": "^5.4.18" + "vite": "^5.4.18", + "zod": "^3.24.2" + }, + "engines": { + "node": ">=16.0.0" } } \ No newline at end of file diff --git a/scripts/verify-dist.js b/scripts/verify-dist.js new file mode 100755 index 0000000..05b88b4 --- /dev/null +++ b/scripts/verify-dist.js @@ -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.'); \ No newline at end of file diff --git a/src/config/index.ts b/src/config/index.ts index 5e93dbf..3d2294c 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -1,7 +1,7 @@ import dotenv from 'dotenv'; -import path from 'path'; import fs from 'fs'; import { McpSettings } from '../types/index.js'; +import { getConfigFilePath } from '../utils/path.js'; dotenv.config(); @@ -14,7 +14,7 @@ const defaultConfig = { }; export const getSettingsPath = (): string => { - return path.resolve(process.cwd(), 'mcp_settings.json'); + return getConfigFilePath('mcp_settings.json', 'Settings'); }; export const loadSettings = (): McpSettings => { diff --git a/src/middlewares/index.ts b/src/middlewares/index.ts index 4aba7e3..a5023a3 100644 --- a/src/middlewares/index.ts +++ b/src/middlewares/index.ts @@ -1,8 +1,36 @@ import express, { Request, Response, NextFunction } from 'express'; import path from 'path'; +import { fileURLToPath } from 'url'; +import { dirname } from 'path'; +import fs from 'fs'; import { auth } from './auth.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 = ( err: Error, _req: Request, @@ -17,7 +45,8 @@ export const errorHandler = ( }; 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) => { if (req.path !== '/sse' && req.path !== '/messages') { @@ -36,7 +65,8 @@ export const initMiddlewares = (app: express.Application): void => { app.use('/api', auth); 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); diff --git a/src/server.ts b/src/server.ts index af9bdc4..b7e3008 100644 --- a/src/server.ts +++ b/src/server.ts @@ -1,6 +1,8 @@ import express from 'express'; import config from './config/index.js'; import path from 'path'; +import { fileURLToPath } from 'url'; +import fs from 'fs'; import { initMcpServer } from './services/mcpService.js'; import { initMiddlewares } from './middlewares/index.js'; import { initRoutes } from './routes/index.js'; @@ -13,9 +15,14 @@ import { import { migrateUserData } from './utils/migration.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 { private app: express.Application; private port: number | string; + private frontendPath: string | null = null; constructor() { this.app = express(); @@ -48,9 +55,8 @@ export class AppServer { throw error; }) .finally(() => { - this.app.get('*', (_req, res) => { - res.sendFile(path.join(process.cwd(), 'frontend', 'dist', 'index.html')); - }); + // Find and serve frontend + this.findAndServeFrontend(); }); } catch (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 { 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`, + ); + } }); } 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'; + + 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; diff --git a/src/services/marketService.ts b/src/services/marketService.ts index 7e7736e..bdde610 100644 --- a/src/services/marketService.ts +++ b/src/services/marketService.ts @@ -1,10 +1,10 @@ import fs from 'fs'; -import path from 'path'; import { MarketServer } from '../types/index.js'; +import { getConfigFilePath } from '../utils/path.js'; // Get path to the servers.json file export const getServersJsonPath = (): string => { - return path.resolve(process.cwd(), 'servers.json'); + return getConfigFilePath('servers.json', 'Servers'); }; // Load all market servers from servers.json diff --git a/src/utils/path.ts b/src/utils/path.ts new file mode 100644 index 0000000..b3b6efa --- /dev/null +++ b/src/utils/path.ts @@ -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; +}; \ No newline at end of file