File size: 6,421 Bytes
eb846d0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
import express from 'express';
import config from './config/index.js';
import path from 'path';
import { fileURLToPath } from 'url';
import fs from 'fs';
import { initUpstreamServers } from './services/mcpService.js';
import { initMiddlewares } from './middlewares/index.js';
import { initRoutes } from './routes/index.js';
import {
  handleSseConnection,
  handleSseMessage,
  handleMcpPostRequest,
  handleMcpOtherRequest,
} from './services/sseService.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;
  private basePath: string;

  constructor() {
    this.app = express();
    this.port = config.port;
    this.basePath = config.basePath;
  }

  async initialize(): Promise<void> {
    try {
      // Initialize default admin user if no users exist
      await initializeDefaultUser();

      initMiddlewares(this.app);
      initRoutes(this.app);
      console.log('Server initialized successfully');

      initUpstreamServers()
        .then(() => {
          console.log('MCP server initialized successfully');
          this.app.get(`${this.basePath}/sse/:group?`, (req, res) => handleSseConnection(req, res));
          this.app.post(`${this.basePath}/messages`, handleSseMessage);
          this.app.post(`${this.basePath}/mcp/:group?`, handleMcpPostRequest);
          this.app.get(`${this.basePath}/mcp/:group?`, handleMcpOtherRequest);
          this.app.delete(`${this.basePath}/mcp/:group?`, 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`,
        );
      }
    });
  }

  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;