diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 4d27658..a082da7 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -1,50 +1,237 @@ # MCPHub Coding Instructions +**ALWAYS follow these instructions first and only fallback to additional search and context gathering if the information here is incomplete or found to be in error.** + ## Project Overview -MCPHub is a TypeScript/Node.js MCP server management hub that provides unified access through HTTP endpoints. +MCPHub is a TypeScript/Node.js MCP (Model Context Protocol) server management hub that provides unified access through HTTP endpoints. It serves as a centralized dashboard for managing multiple MCP servers with real-time monitoring, authentication, and flexible routing. **Core Components:** - **Backend**: Express.js + TypeScript + ESM (`src/server.ts`) - **Frontend**: React/Vite + Tailwind CSS (`frontend/`) - **MCP Integration**: Connects multiple MCP servers (`src/services/mcpService.ts`) +- **Authentication**: JWT-based with bcrypt password hashing +- **Configuration**: JSON-based MCP server definitions (`mcp_settings.json`) -## Development Environment +## Working Effectively + +### Bootstrap and Setup (CRITICAL - Follow Exact Steps) ```bash +# Install pnpm if not available +npm install -g pnpm + +# Install dependencies - takes ~30 seconds pnpm install -pnpm dev # Start both backend and frontend -pnpm backend:dev # Backend only -pnpm frontend:dev # Frontend only + +# Setup environment (optional) +cp .env.example .env + +# Build and test to verify setup +pnpm lint # ~3 seconds - NEVER CANCEL +pnpm backend:build # ~5 seconds - NEVER CANCEL +pnpm test:ci # ~16 seconds - NEVER CANCEL. Set timeout to 60+ seconds +pnpm frontend:build # ~5 seconds - NEVER CANCEL +pnpm build # ~10 seconds total - NEVER CANCEL. Set timeout to 60+ seconds ``` -## Project Conventions +**CRITICAL TIMING**: These commands are fast but NEVER CANCEL them. Always wait for completion. -### File Structure +### Development Environment -- `src/services/` - Core business logic -- `src/controllers/` - HTTP request handlers -- `src/types/index.ts` - TypeScript type definitions +```bash +# Start both backend and frontend (recommended for most development) +pnpm dev # Backend on :3001, Frontend on :5173 + +# OR start separately (required on Windows, optional on Linux/macOS) +# Terminal 1: Backend only +pnpm backend:dev # Runs on port 3000 (or PORT env var) + +# Terminal 2: Frontend only +pnpm frontend:dev # Runs on port 5173, proxies API to backend +``` + +**NEVER CANCEL**: Development servers may take 10-15 seconds to fully initialize all MCP servers. + +### Build Commands (Production) + +```bash +# Full production build - takes ~10 seconds total +pnpm build # NEVER CANCEL - Set timeout to 60+ seconds + +# Individual builds +pnpm backend:build # TypeScript compilation - ~5 seconds +pnpm frontend:build # Vite build - ~5 seconds + +# Start production server +pnpm start # Requires dist/ and frontend/dist/ to exist +``` + +### Testing and Validation + +```bash +# Run all tests - takes ~16 seconds with 73 tests +pnpm test:ci # NEVER CANCEL - Set timeout to 60+ seconds + +# Development testing +pnpm test # Interactive mode +pnpm test:watch # Watch mode for development +pnpm test:coverage # With coverage report + +# Code quality +pnpm lint # ESLint - ~3 seconds +pnpm format # Prettier formatting - ~3 seconds +``` + +**CRITICAL**: All tests MUST pass before committing. Do not modify tests to make them pass unless specifically required for your changes. + +## Manual Validation Requirements + +**ALWAYS perform these validation steps after making changes:** + +### 1. Basic Application Functionality +```bash +# Start the application +pnpm dev + +# Verify backend responds (in another terminal) +curl http://localhost:3000/api/health +# Expected: Should return health status + +# Verify frontend serves +curl -I http://localhost:3000/ +# Expected: HTTP 200 OK with HTML content +``` + +### 2. MCP Server Integration Test +```bash +# Check MCP servers are loading (look for log messages) +# Expected log output should include: +# - "Successfully connected client for server: [name]" +# - "Successfully listed [N] tools for server: [name]" +# - Some servers may fail due to missing API keys (normal in dev) +``` + +### 3. Build Verification +```bash +# Verify production build works +pnpm build +node scripts/verify-dist.js +# Expected: "โœ… Verification passed! Frontend and backend dist files are present." +``` + +**NEVER skip these validation steps**. If any fail, debug and fix before proceeding. + +## Project Structure and Key Files + +### Critical Backend Files +- `src/index.ts` - Application entry point +- `src/server.ts` - Express server setup and middleware +- `src/services/mcpService.ts` - **Core MCP server management logic** - `src/config/index.ts` - Configuration management +- `src/routes/` - HTTP route definitions +- `src/controllers/` - HTTP request handlers +- `src/dao/` - Data access layer for users, groups, servers +- `src/types/index.ts` - TypeScript type definitions -### Key Notes +### Critical Frontend Files +- `frontend/src/` - React application source +- `frontend/src/pages/` - Page components (development entry point) +- `frontend/src/components/` - Reusable UI components -- Use ESM modules: Import with `.js` extensions, not `.ts` -- Configuration file: `mcp_settings.json` -- Endpoint formats: `/mcp/{group|server}` and `/mcp/$smart` -- All code comments must be written in English -- Frontend uses i18n with resource files in `locales/` folder -- Server-side code should use appropriate abstraction layers for extensibility and replaceability +### Configuration Files +- `mcp_settings.json` - **MCP server definitions and user accounts** +- `package.json` - Dependencies and scripts +- `tsconfig.json` - TypeScript configuration +- `jest.config.cjs` - Test configuration +- `.eslintrc.json` - Linting rules -## Development Process +### Docker and Deployment +- `Dockerfile` - Multi-stage build with Python base + Node.js +- `entrypoint.sh` - Docker startup script +- `bin/cli.js` - NPM package CLI entry point -- For complex features, implement step by step and wait for confirmation before proceeding to the next step -- After implementing features, no separate summary documentation is needed - update README.md and README.zh.md as appropriate +## Development Process and Conventions + +### Code Style Requirements +- **ESM modules**: Always use `.js` extensions in imports, not `.ts` +- **English only**: All code comments must be written in English +- **TypeScript strict**: Follow strict type checking rules +- **Import style**: `import { something } from './file.js'` (note .js extension) + +### Key Configuration Notes +- **MCP servers**: Defined in `mcp_settings.json` with command/args +- **Endpoints**: `/mcp/{group|server}` and `/mcp/$smart` for routing +- **i18n**: Frontend uses react-i18next with files in `locales/` folder +- **Authentication**: JWT tokens with bcrypt password hashing +- **Default credentials**: admin/admin123 (configured in mcp_settings.json) ### Development Entry Points +- **Add MCP server**: Modify `mcp_settings.json` and restart +- **New API endpoint**: Add route in `src/routes/`, controller in `src/controllers/` +- **Frontend feature**: Start from `frontend/src/pages/` or `frontend/src/components/` +- **Add tests**: Follow patterns in `tests/` directory -- **MCP Servers**: Modify `src/services/mcpService.ts` -- **API Endpoints**: Add routes in `src/routes/`, controllers in `src/controllers/` -- **Frontend Features**: Start from `frontend/src/pages/` -- **Testing**: Follow existing patterns in `tests/` +### Common Development Tasks + +#### Adding a new MCP server: +1. Add server definition to `mcp_settings.json` +2. Restart backend to load new server +3. Check logs for successful connection +4. Test via dashboard or API endpoints + +#### API development: +1. Define route in `src/routes/` +2. Implement controller in `src/controllers/` +3. Add types in `src/types/index.ts` if needed +4. Write tests in `tests/controllers/` + +#### Frontend development: +1. Create/modify components in `frontend/src/components/` +2. Add pages in `frontend/src/pages/` +3. Update routing if needed +4. Test in development mode with `pnpm frontend:dev` + +## Validation and CI Requirements + +### Before Committing - ALWAYS Run: +```bash +pnpm lint # Must pass - ~3 seconds +pnpm backend:build # Must compile - ~5 seconds +pnpm test:ci # All tests must pass - ~16 seconds +pnpm build # Full build must work - ~10 seconds +``` + +**CRITICAL**: CI will fail if any of these commands fail. Fix issues locally first. + +### CI Pipeline (.github/workflows/ci.yml) +- Runs on Node.js 20.x +- Tests: linting, type checking, unit tests with coverage +- **NEVER CANCEL**: CI builds may take 2-3 minutes total + +## Troubleshooting + +### Common Issues +- **"uvx command not found"**: Some MCP servers require `uvx` (Python package manager) - this is expected in development +- **Port already in use**: Change PORT environment variable or kill existing processes +- **Frontend not loading**: Ensure frontend was built with `pnpm frontend:build` +- **MCP server connection failed**: Check server command/args in `mcp_settings.json` + +### Build Failures +- **TypeScript errors**: Run `pnpm backend:build` to see compilation errors +- **Test failures**: Run `pnpm test:verbose` for detailed test output +- **Lint errors**: Run `pnpm lint` and fix reported issues + +### Development Issues +- **Backend not starting**: Check for port conflicts, verify `mcp_settings.json` syntax +- **Frontend proxy errors**: Ensure backend is running before starting frontend +- **Hot reload not working**: Restart development server + +## Performance Notes +- **Install time**: pnpm install takes ~30 seconds +- **Build time**: Full build takes ~10 seconds +- **Test time**: Complete test suite takes ~16 seconds +- **Startup time**: Backend initialization takes 10-15 seconds (MCP server connections) + +**Remember**: NEVER CANCEL any build or test commands. Always wait for completion even if they seem slow. diff --git a/src/config/DaoConfigService.ts b/src/config/DaoConfigService.ts index c0eb0a7..84b9e52 100644 --- a/src/config/DaoConfigService.ts +++ b/src/config/DaoConfigService.ts @@ -1,16 +1,16 @@ import { McpSettings, IUser, ServerConfig } from '../types/index.js'; -import { - UserDao, - ServerDao, - GroupDao, - SystemConfigDao, +import { + UserDao, + ServerDao, + GroupDao, + SystemConfigDao, UserConfigDao, ServerConfigWithName, UserDaoImpl, ServerDaoImpl, GroupDaoImpl, SystemConfigDaoImpl, - UserConfigDaoImpl + UserConfigDaoImpl, } from '../dao/index.js'; /** @@ -22,7 +22,7 @@ export class DaoConfigService { private serverDao: ServerDao, private groupDao: GroupDao, private systemConfigDao: SystemConfigDao, - private userConfigDao: UserConfigDao + private userConfigDao: UserConfigDao, ) {} /** @@ -34,7 +34,7 @@ export class DaoConfigService { this.serverDao.findAll(), this.groupDao.findAll(), this.systemConfigDao.get(), - this.userConfigDao.getAll() + this.userConfigDao.getAll(), ]); // Convert servers back to the original format @@ -49,7 +49,7 @@ export class DaoConfigService { mcpServers, groups, systemConfig, - userConfigs + userConfigs, }; // Apply user-specific filtering if needed @@ -96,7 +96,7 @@ export class DaoConfigService { if (settings.mcpServers) { const currentServers = await this.serverDao.findAll(); const currentServerNames = new Set(currentServers.map((s: ServerConfigWithName) => s.name)); - + for (const [name, config] of Object.entries(settings.mcpServers)) { const serverWithName: ServerConfigWithName = { name, ...config }; if (currentServerNames.has(name)) { @@ -118,7 +118,7 @@ export class DaoConfigService { if (settings.groups) { const currentGroups = await this.groupDao.findAll(); const currentGroupIds = new Set(currentGroups.map((g: any) => g.id)); - + for (const group of settings.groups) { if (group.id && currentGroupIds.has(group.id)) { promises.push(this.groupDao.update(group.id, group)); @@ -128,7 +128,7 @@ export class DaoConfigService { } // Remove groups that are no longer in the settings - const newGroupIds = new Set(settings.groups.map(g => g.id).filter(Boolean)); + const newGroupIds = new Set(settings.groups.map((g) => g.id).filter(Boolean)); for (const existingGroup of currentGroups) { if (!newGroupIds.has(existingGroup.id)) { promises.push(this.groupDao.delete(existingGroup.id)); @@ -173,7 +173,7 @@ export class DaoConfigService { } const filteredGroups = (settings.groups || []).filter( - group => group.owner === user.username || group.owner === undefined + (group) => group.owner === user.username || group.owner === undefined, ); return { @@ -182,7 +182,7 @@ export class DaoConfigService { groups: filteredGroups, users: [], // Non-admin users can't see user list systemConfig: {}, // Non-admin users can't see system config - userConfigs: { [user.username]: settings.userConfigs?.[user.username] || {} } + userConfigs: { [user.username]: settings.userConfigs?.[user.username] || {} }, }; } @@ -190,9 +190,9 @@ export class DaoConfigService { * Merge settings for non-admin users */ private mergeSettingsForUser( - currentSettings: McpSettings, - newSettings: McpSettings, - user: IUser + currentSettings: McpSettings, + newSettings: McpSettings, + user: IUser, ): McpSettings { if (user.isAdmin) { return newSettings; @@ -214,14 +214,14 @@ export class DaoConfigService { // Merge groups (only user's own groups) if (newSettings.groups) { - const userGroups = newSettings.groups.filter( - group => !group.owner || group.owner === user.username - ).map(group => ({ ...group, owner: user.username })); - + const userGroups = newSettings.groups + .filter((group) => !group.owner || group.owner === user.username) + .map((group) => ({ ...group, owner: user.username })); + const otherGroups = (currentSettings.groups || []).filter( - group => group.owner !== user.username + (group) => group.owner !== user.username, ); - + mergedSettings.groups = [...otherGroups, ...userGroups]; } @@ -260,6 +260,6 @@ export function createDaoConfigService(): DaoConfigService { new ServerDaoImpl(), new GroupDaoImpl(), new SystemConfigDaoImpl(), - new UserConfigDaoImpl() + new UserConfigDaoImpl(), ); } diff --git a/src/config/configManager.ts b/src/config/configManager.ts index 11e86e1..fc104a3 100644 --- a/src/config/configManager.ts +++ b/src/config/configManager.ts @@ -4,7 +4,11 @@ import { getPackageVersion } from '../utils/version.js'; import { getDataService } from '../services/services.js'; import { DataService } from '../services/dataService.js'; import { DaoConfigService, createDaoConfigService } from './DaoConfigService.js'; -import { loadOriginalSettings as legacyLoadSettings, saveSettings as legacySaveSettings, clearSettingsCache as legacyClearCache } from './index.js'; +import { + loadOriginalSettings as legacyLoadSettings, + saveSettings as legacySaveSettings, + clearSettingsCache as legacyClearCache, +} from './index.js'; dotenv.config(); @@ -71,12 +75,12 @@ export const getSettingsCacheInfo = (): { hasCache: boolean; usingDao: boolean } const daoInfo = daoConfigService.getCacheInfo(); return { ...daoInfo, - usingDao: true + usingDao: true, }; } else { return { hasCache: false, // Legacy method doesn't expose cache info here - usingDao: false + usingDao: false, }; } }; @@ -108,14 +112,14 @@ export const getDaoConfigService = (): DaoConfigService => { export const migrateToDao = async (): Promise => { try { console.log('Starting migration from legacy format to DAO layer...'); - + // Load data using legacy method const legacySettings = legacyLoadSettings(); - + // Save using DAO layer switchToDao(); const success = await saveSettings(legacySettings); - + if (success) { console.log('Migration completed successfully'); return true; diff --git a/src/config/migrationUtils.ts b/src/config/migrationUtils.ts index 1ce998f..0d9f4b0 100644 --- a/src/config/migrationUtils.ts +++ b/src/config/migrationUtils.ts @@ -2,12 +2,7 @@ * Migration utilities for moving from legacy file-based config to DAO layer */ -import { - loadSettings, - migrateToDao, - switchToDao, - switchToLegacy -} from './configManager.js'; +import { loadSettings, migrateToDao, switchToDao, switchToLegacy } from './configManager.js'; import { UserDaoImpl, ServerDaoImpl, GroupDaoImpl } from '../dao/index.js'; /** @@ -16,42 +11,41 @@ import { UserDaoImpl, ServerDaoImpl, GroupDaoImpl } from '../dao/index.js'; export async function validateMigration(): Promise { try { console.log('Validating migration...'); - + // Load settings using DAO layer switchToDao(); const daoSettings = await loadSettings(); - + // Load settings using legacy method switchToLegacy(); const legacySettings = await loadSettings(); - + // Compare key metrics const daoUserCount = daoSettings.users?.length || 0; const legacyUserCount = legacySettings.users?.length || 0; - + const daoServerCount = Object.keys(daoSettings.mcpServers || {}).length; const legacyServerCount = Object.keys(legacySettings.mcpServers || {}).length; - + const daoGroupCount = daoSettings.groups?.length || 0; const legacyGroupCount = legacySettings.groups?.length || 0; - + console.log('Data comparison:'); console.log(`Users: DAO=${daoUserCount}, Legacy=${legacyUserCount}`); console.log(`Servers: DAO=${daoServerCount}, Legacy=${legacyServerCount}`); console.log(`Groups: DAO=${daoGroupCount}, Legacy=${legacyGroupCount}`); - - const isValid = ( + + const isValid = daoUserCount === legacyUserCount && daoServerCount === legacyServerCount && - daoGroupCount === legacyGroupCount - ); - + daoGroupCount === legacyGroupCount; + if (isValid) { console.log('โœ… Migration validation passed'); } else { console.log('โŒ Migration validation failed'); } - + return isValid; } catch (error) { console.error('Migration validation error:', error); @@ -65,34 +59,34 @@ export async function validateMigration(): Promise { export async function performMigration(): Promise { try { console.log('๐Ÿš€ Starting migration to DAO layer...'); - + // Step 1: Backup current data console.log('๐Ÿ“ Creating backup of current data...'); switchToLegacy(); const _backupData = await loadSettings(); - + // Step 2: Perform migration console.log('๐Ÿ”„ Migrating data to DAO layer...'); const migrationSuccess = await migrateToDao(); - + if (!migrationSuccess) { console.error('โŒ Migration failed'); return false; } - + // Step 3: Validate migration console.log('๐Ÿ” Validating migration...'); const validationSuccess = await validateMigration(); - + if (!validationSuccess) { console.error('โŒ Migration validation failed'); // Could implement rollback here if needed return false; } - + console.log('โœ… Migration completed successfully!'); console.log('๐Ÿ’ก You can now use the DAO layer by setting USE_DAO_LAYER=true'); - + return true; } catch (error) { console.error('Migration error:', error); @@ -106,23 +100,23 @@ export async function performMigration(): Promise { export async function testDaoOperations(): Promise { try { console.log('๐Ÿงช Testing DAO operations...'); - + switchToDao(); const userDao = new UserDaoImpl(); const serverDao = new ServerDaoImpl(); const groupDao = new GroupDaoImpl(); - + // Test user operations console.log('Testing user operations...'); const testUser = await userDao.createWithHashedPassword('test-dao-user', 'password123', false); console.log(`โœ… Created test user: ${testUser.username}`); - + const foundUser = await userDao.findByUsername('test-dao-user'); console.log(`โœ… Found user: ${foundUser?.username}`); - + const isValidPassword = await userDao.validateCredentials('test-dao-user', 'password123'); console.log(`โœ… Password validation: ${isValidPassword}`); - + // Test server operations console.log('Testing server operations...'); const testServer = await serverDao.create({ @@ -130,33 +124,33 @@ export async function testDaoOperations(): Promise { command: 'node', args: ['test.js'], enabled: true, - owner: 'test-dao-user' + owner: 'test-dao-user', }); console.log(`โœ… Created test server: ${testServer.name}`); - + const userServers = await serverDao.findByOwner('test-dao-user'); console.log(`โœ… Found ${userServers.length} servers for user`); - + // Test group operations console.log('Testing group operations...'); const testGroup = await groupDao.create({ name: 'test-dao-group', description: 'Test group for DAO operations', servers: ['test-dao-server'], - owner: 'test-dao-user' + owner: 'test-dao-user', }); console.log(`โœ… Created test group: ${testGroup.name} (ID: ${testGroup.id})`); - + const userGroups = await groupDao.findByOwner('test-dao-user'); console.log(`โœ… Found ${userGroups.length} groups for user`); - + // Cleanup test data console.log('Cleaning up test data...'); await groupDao.delete(testGroup.id); await serverDao.delete('test-dao-server'); await userDao.delete('test-dao-user'); console.log('โœ… Test data cleaned up'); - + console.log('๐ŸŽ‰ All DAO operations test passed!'); return true; } catch (error) { @@ -171,7 +165,7 @@ export async function testDaoOperations(): Promise { export async function performanceComparison(): Promise { try { console.log('โšก Performance comparison...'); - + // Test legacy approach console.log('Testing legacy approach...'); switchToLegacy(); @@ -179,7 +173,7 @@ export async function performanceComparison(): Promise { await loadSettings(); const legacyTime = Date.now() - legacyStart; console.log(`Legacy load time: ${legacyTime}ms`); - + // Test DAO approach console.log('Testing DAO approach...'); switchToDao(); @@ -187,13 +181,13 @@ export async function performanceComparison(): Promise { await loadSettings(); const daoTime = Date.now() - daoStart; console.log(`DAO load time: ${daoTime}ms`); - + // Comparison const difference = daoTime - legacyTime; const percentage = ((difference / legacyTime) * 100).toFixed(2); - + console.log(`Performance difference: ${difference}ms (${percentage}%)`); - + if (difference > 0) { console.log(`DAO approach is ${percentage}% slower`); } else { @@ -210,14 +204,14 @@ export async function performanceComparison(): Promise { export async function generateMigrationReport(): Promise { try { console.log('๐Ÿ“Š Generating migration report...'); - + // Collect statistics from both approaches switchToLegacy(); const legacySettings = await loadSettings(); - + switchToDao(); const daoSettings = await loadSettings(); - + const report = { timestamp: new Date().toISOString(), legacy: { @@ -225,20 +219,20 @@ export async function generateMigrationReport(): Promise { servers: Object.keys(legacySettings.mcpServers || {}).length, groups: legacySettings.groups?.length || 0, systemConfigSections: Object.keys(legacySettings.systemConfig || {}).length, - userConfigs: Object.keys(legacySettings.userConfigs || {}).length + userConfigs: Object.keys(legacySettings.userConfigs || {}).length, }, dao: { users: daoSettings.users?.length || 0, servers: Object.keys(daoSettings.mcpServers || {}).length, groups: daoSettings.groups?.length || 0, systemConfigSections: Object.keys(daoSettings.systemConfig || {}).length, - userConfigs: Object.keys(daoSettings.userConfigs || {}).length - } + userConfigs: Object.keys(daoSettings.userConfigs || {}).length, + }, }; - + console.log('๐Ÿ“ˆ Migration Report:'); console.log(JSON.stringify(report, null, 2)); - + return report; } catch (error) { console.error('Report generation error:', error); diff --git a/src/controllers/logController.ts b/src/controllers/logController.ts index 0b85761..73e8674 100644 --- a/src/controllers/logController.ts +++ b/src/controllers/logController.ts @@ -31,7 +31,7 @@ export const streamLogs = (req: Request, res: Response): void => { res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', - 'Connection': 'keep-alive' + Connection: 'keep-alive', }); // Send initial data @@ -52,4 +52,4 @@ export const streamLogs = (req: Request, res: Response): void => { console.error('Error streaming logs:', error); res.status(500).json({ success: false, error: 'Error streaming logs' }); } -}; \ No newline at end of file +}; diff --git a/src/controllers/marketController.ts b/src/controllers/marketController.ts index b80fb0c..6ec7f4e 100644 --- a/src/controllers/marketController.ts +++ b/src/controllers/marketController.ts @@ -7,7 +7,7 @@ import { getMarketTags, searchMarketServers, filterMarketServersByCategory, - filterMarketServersByTag + filterMarketServersByTag, } from '../services/marketService.js'; // Get all market servers @@ -100,7 +100,7 @@ export const searchMarketServersByQuery = (req: Request, res: Response): void => try { const { query } = req.query; const searchQuery = typeof query === 'string' ? query : ''; - + const servers = searchMarketServers(searchQuery); const response: ApiResponse = { success: true, @@ -119,7 +119,7 @@ export const searchMarketServersByQuery = (req: Request, res: Response): void => export const getMarketServersByCategory = (req: Request, res: Response): void => { try { const { category } = req.params; - + const servers = filterMarketServersByCategory(category); const response: ApiResponse = { success: true, @@ -138,7 +138,7 @@ export const getMarketServersByCategory = (req: Request, res: Response): void => export const getMarketServersByTag = (req: Request, res: Response): void => { try { const { tag } = req.params; - + const servers = filterMarketServersByTag(tag); const response: ApiResponse = { success: true, @@ -151,4 +151,4 @@ export const getMarketServersByTag = (req: Request, res: Response): void => { message: 'Failed to filter market servers by tag', }); } -}; \ No newline at end of file +}; diff --git a/src/controllers/promptController.ts b/src/controllers/promptController.ts index dcfc486..054cb38 100644 --- a/src/controllers/promptController.ts +++ b/src/controllers/promptController.ts @@ -17,7 +17,7 @@ export const getPrompt = async (req: Request, res: Response): Promise => { } const promptArgs = { - params: req.body as { [key: string]: any } + params: req.body as { [key: string]: any }, }; const result = await handleGetPromptRequest(promptArgs, serverName); if (result.isError) { diff --git a/src/dao/DaoFactory.ts b/src/dao/DaoFactory.ts index 0b45c05..780bb23 100644 --- a/src/dao/DaoFactory.ts +++ b/src/dao/DaoFactory.ts @@ -20,7 +20,7 @@ export interface DaoFactory { */ export class JsonFileDaoFactory implements DaoFactory { private static instance: JsonFileDaoFactory; - + private userDao: UserDao | null = null; private serverDao: ServerDao | null = null; private groupDao: GroupDao | null = null; diff --git a/src/dao/SystemConfigDao.ts b/src/dao/SystemConfigDao.ts index bd1f204..4e11723 100644 --- a/src/dao/SystemConfigDao.ts +++ b/src/dao/SystemConfigDao.ts @@ -43,11 +43,11 @@ export class SystemConfigDaoImpl extends JsonFileBaseDao implements SystemConfig async update(config: Partial): Promise { const settings = await this.loadSettings(); const currentConfig = settings.systemConfig || {}; - + // Deep merge configuration const updatedConfig = this.deepMerge(currentConfig, config); settings.systemConfig = updatedConfig; - + await this.saveSettings(settings); return updatedConfig; } @@ -55,10 +55,10 @@ export class SystemConfigDaoImpl extends JsonFileBaseDao implements SystemConfig async reset(): Promise { const settings = await this.loadSettings(); const defaultConfig: SystemConfig = {}; - + settings.systemConfig = defaultConfig; await this.saveSettings(settings); - + return defaultConfig; } @@ -67,7 +67,10 @@ export class SystemConfigDaoImpl extends JsonFileBaseDao implements SystemConfig return config[section]; } - async updateSection(section: K, value: SystemConfig[K]): Promise { + async updateSection( + section: K, + value: SystemConfig[K], + ): Promise { try { await this.update({ [section]: value } as Partial); return true; @@ -81,7 +84,7 @@ export class SystemConfigDaoImpl extends JsonFileBaseDao implements SystemConfig */ private deepMerge(target: any, source: any): any { const result = { ...target }; - + for (const key in source) { if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) { result[key] = this.deepMerge(target[key] || {}, source[key]); @@ -89,7 +92,7 @@ export class SystemConfigDaoImpl extends JsonFileBaseDao implements SystemConfig result[key] = source[key]; } } - + return result; } } diff --git a/src/dao/UserConfigDao.ts b/src/dao/UserConfigDao.ts index a4c9b44..e8abe08 100644 --- a/src/dao/UserConfigDao.ts +++ b/src/dao/UserConfigDao.ts @@ -38,12 +38,19 @@ export interface UserConfigDao { /** * Get specific configuration section for user */ - getSection(username: string, section: K): Promise; + getSection( + username: string, + section: K, + ): Promise; /** * Update specific configuration section for user */ - updateSection(username: string, section: K, value: UserConfig[K]): Promise; + updateSection( + username: string, + section: K, + value: UserConfig[K], + ): Promise; } /** @@ -62,28 +69,28 @@ export class UserConfigDaoImpl extends JsonFileBaseDao implements UserConfigDao async update(username: string, config: Partial): Promise { const settings = await this.loadSettings(); - + if (!settings.userConfigs) { settings.userConfigs = {}; } - + const currentConfig = settings.userConfigs[username] || {}; - + // Deep merge configuration const updatedConfig = this.deepMerge(currentConfig, config); settings.userConfigs[username] = updatedConfig; - + await this.saveSettings(settings); return updatedConfig; } async delete(username: string): Promise { const settings = await this.loadSettings(); - + if (!settings.userConfigs || !settings.userConfigs[username]) { return false; } - + delete settings.userConfigs[username]; await this.saveSettings(settings); return true; @@ -99,12 +106,19 @@ export class UserConfigDaoImpl extends JsonFileBaseDao implements UserConfigDao return this.update(username, defaultConfig); } - async getSection(username: string, section: K): Promise { + async getSection( + username: string, + section: K, + ): Promise { const config = await this.get(username); return config?.[section]; } - async updateSection(username: string, section: K, value: UserConfig[K]): Promise { + async updateSection( + username: string, + section: K, + value: UserConfig[K], + ): Promise { try { await this.update(username, { [section]: value } as Partial); return true; @@ -118,7 +132,7 @@ export class UserConfigDaoImpl extends JsonFileBaseDao implements UserConfigDao */ private deepMerge(target: any, source: any): any { const result = { ...target }; - + for (const key in source) { if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) { result[key] = this.deepMerge(target[key] || {}, source[key]); @@ -126,7 +140,7 @@ export class UserConfigDaoImpl extends JsonFileBaseDao implements UserConfigDao result[key] = source[key]; } } - + return result; } } diff --git a/src/dao/base/BaseDao.ts b/src/dao/base/BaseDao.ts index 7d45667..1e4ba5c 100644 --- a/src/dao/base/BaseDao.ts +++ b/src/dao/base/BaseDao.ts @@ -54,38 +54,38 @@ export abstract class BaseDaoImpl implements BaseDao { async findById(id: K): Promise { const entities = await this.getAll(); - return entities.find(entity => this.getEntityId(entity) === id) || null; + return entities.find((entity) => this.getEntityId(entity) === id) || null; } async create(data: Omit): Promise { const entities = await this.getAll(); const newEntity = this.createEntity(data); - + entities.push(newEntity); await this.saveAll(entities); - + return newEntity; } async update(id: K, updates: Partial): Promise { const entities = await this.getAll(); - const index = entities.findIndex(entity => this.getEntityId(entity) === id); - + const index = entities.findIndex((entity) => this.getEntityId(entity) === id); + if (index === -1) { return null; } const updatedEntity = this.updateEntity(entities[index], updates); entities[index] = updatedEntity; - + await this.saveAll(entities); return updatedEntity; } async delete(id: K): Promise { const entities = await this.getAll(); - const index = entities.findIndex(entity => this.getEntityId(entity) === id); - + const index = entities.findIndex((entity) => this.getEntityId(entity) === id); + if (index === -1) { return false; } diff --git a/src/dao/examples.ts b/src/dao/examples.ts index bc81a6c..e0870ab 100644 --- a/src/dao/examples.ts +++ b/src/dao/examples.ts @@ -1,18 +1,18 @@ /** * Data access layer example and test utilities - * + * * This file demonstrates how to use the DAO layer for managing different types of data * in the MCPHub application. */ -import { - getUserDao, - getServerDao, - getGroupDao, - getSystemConfigDao, +import { + getUserDao, + getServerDao, + getGroupDao, + getSystemConfigDao, getUserConfigDao, JsonFileDaoFactory, - setDaoFactory + setDaoFactory, } from './DaoFactory.js'; /** @@ -39,7 +39,10 @@ export async function exampleUserOperations() { // Find all admin users const admins = await userDao.findAdmins(); - console.log('Admin users:', admins.map(u => u.username)); + console.log( + 'Admin users:', + admins.map((u) => u.username), + ); // Delete user await userDao.delete('testuser'); @@ -58,21 +61,27 @@ export async function exampleServerOperations() { command: 'node', args: ['server.js'], enabled: true, - owner: 'admin' + owner: 'admin', }); console.log('Created server:', newServer.name); // Find servers by owner const userServers = await serverDao.findByOwner('admin'); - console.log('Servers owned by admin:', userServers.map(s => s.name)); + console.log( + 'Servers owned by admin:', + userServers.map((s) => s.name), + ); // Find enabled servers const enabledServers = await serverDao.findEnabled(); - console.log('Enabled servers:', enabledServers.map(s => s.name)); + console.log( + 'Enabled servers:', + enabledServers.map((s) => s.name), + ); // Update server tools await serverDao.updateTools('test-server', { - 'tool1': { enabled: true, description: 'Test tool' } + tool1: { enabled: true, description: 'Test tool' }, }); console.log('Updated server tools'); @@ -92,13 +101,16 @@ export async function exampleGroupOperations() { name: 'test-group', description: 'Test group for development', servers: ['server1', 'server2'], - owner: 'admin' + owner: 'admin', }); console.log('Created group:', newGroup.name, 'with ID:', newGroup.id); // Find groups by owner const userGroups = await groupDao.findByOwner('admin'); - console.log('Groups owned by admin:', userGroups.map(g => g.name)); + console.log( + 'Groups owned by admin:', + userGroups.map((g) => g.name), + ); // Add server to group await groupDao.addServerToGroup(newGroup.id, 'server3'); @@ -106,7 +118,10 @@ export async function exampleGroupOperations() { // Find groups containing specific server const groupsWithServer = await groupDao.findByServer('server1'); - console.log('Groups containing server1:', groupsWithServer.map(g => g.name)); + console.log( + 'Groups containing server1:', + groupsWithServer.map((g) => g.name), + ); // Remove server from group await groupDao.removeServerFromGroup(newGroup.id, 'server2'); @@ -131,7 +146,7 @@ export async function exampleSystemConfigOperations() { await systemConfigDao.updateSection('routing', { enableGlobalRoute: true, enableGroupNameRoute: true, - enableBearerAuth: false + enableBearerAuth: false, }); console.log('Updated routing configuration'); @@ -139,7 +154,7 @@ export async function exampleSystemConfigOperations() { await systemConfigDao.updateSection('install', { pythonIndexUrl: 'https://pypi.org/simple/', npmRegistry: 'https://registry.npmjs.org/', - baseUrl: 'https://mcphub.local' + baseUrl: 'https://mcphub.local', }); console.log('Updated install configuration'); @@ -158,8 +173,8 @@ export async function exampleUserConfigOperations() { await userConfigDao.update('admin', { routing: { enableGlobalRoute: false, - enableGroupNameRoute: true - } + enableGroupNameRoute: true, + }, }); console.log('Updated admin user config'); @@ -186,22 +201,22 @@ export async function exampleUserConfigOperations() { export async function testAllDaoOperations() { try { console.log('=== Testing DAO Layer ==='); - + console.log('\n--- User Operations ---'); await exampleUserOperations(); - + console.log('\n--- Server Operations ---'); await exampleServerOperations(); - + console.log('\n--- Group Operations ---'); await exampleGroupOperations(); - + console.log('\n--- System Config Operations ---'); await exampleSystemConfigOperations(); - + console.log('\n--- User Config Operations ---'); await exampleUserConfigOperations(); - + console.log('\n=== DAO Layer Test Complete ==='); } catch (error) { console.error('Error during DAO testing:', error); diff --git a/src/services/sseService.ts b/src/services/sseService.ts index 027a433..4a063ce 100644 --- a/src/services/sseService.ts +++ b/src/services/sseService.ts @@ -43,7 +43,7 @@ export const handleSseConnection = async (req: Request, res: Response): Promise< const userContextService = UserContextService.getInstance(); const currentUser = userContextService.getCurrentUser(); const username = currentUser?.username; - + // Check bearer auth using filtered settings if (!validateBearerAuth(req)) { console.warn('Bearer authentication failed or not provided'); @@ -74,7 +74,7 @@ export const handleSseConnection = async (req: Request, res: Response): Promise< } // Construct the appropriate messages path based on user context - const messagesPath = username + const messagesPath = username ? `${config.basePath}/${username}/messages` : `${config.basePath}/messages`; @@ -100,7 +100,7 @@ export const handleSseMessage = async (req: Request, res: Response): Promise { const userContextService = UserContextService.getInstance(); const currentUser = userContextService.getCurrentUser(); const username = currentUser?.username; - + console.log(`Handling MCP other request${username ? ` for user: ${username}` : ''}`); - + // Check bearer auth using filtered settings if (!validateBearerAuth(req)) { res.status(401).send('Bearer authentication required or invalid token');