diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index 84aff6a..07cc2b7 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -12,16 +12,7 @@ import GroupsPage from './pages/GroupsPage';
import SettingsPage from './pages/SettingsPage';
import MarketPage from './pages/MarketPage';
import LogsPage from './pages/LogsPage';
-
-// Get base path from environment variable or default to empty string
-const getBasePath = (): string => {
- const basePath = import.meta.env.BASE_PATH || '';
- // Ensure the path starts with / if it's not empty and doesn't already start with /
- if (basePath && !basePath.startsWith('/')) {
- return '/' + basePath;
- }
- return basePath;
-};
+import { getBasePath } from './utils/runtime';
function App() {
const basename = getBasePath();
diff --git a/frontend/src/components/AddServerForm.tsx b/frontend/src/components/AddServerForm.tsx
index 4d2030d..a8e4fee 100644
--- a/frontend/src/components/AddServerForm.tsx
+++ b/frontend/src/components/AddServerForm.tsx
@@ -1,7 +1,7 @@
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import ServerForm from './ServerForm'
-import { getApiUrl } from '../utils/api'
+import { getApiUrl } from '../utils/runtime';
interface AddServerFormProps {
onAdd: () => void
diff --git a/frontend/src/components/EditServerForm.tsx b/frontend/src/components/EditServerForm.tsx
index 3078be7..fce9cb4 100644
--- a/frontend/src/components/EditServerForm.tsx
+++ b/frontend/src/components/EditServerForm.tsx
@@ -1,6 +1,7 @@
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Server } from '@/types'
+import { getApiUrl } from '../utils/runtime'
import ServerForm from './ServerForm'
interface EditServerFormProps {
@@ -17,7 +18,7 @@ const EditServerForm = ({ server, onEdit, onCancel }: EditServerFormProps) => {
try {
setError(null)
const token = localStorage.getItem('mcphub_token');
- const response = await fetch(`/api/servers/${server.name}`, {
+ const response = await fetch(getApiUrl(`/servers/${server.name}`), {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
diff --git a/frontend/src/hooks/useGroupData.ts b/frontend/src/hooks/useGroupData.ts
index 3d48c69..852aa01 100644
--- a/frontend/src/hooks/useGroupData.ts
+++ b/frontend/src/hooks/useGroupData.ts
@@ -1,7 +1,7 @@
import { useState, useEffect, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { Group, ApiResponse } from '@/types';
-import { getApiUrl } from '../utils/api';
+import { getApiUrl } from '../utils/runtime';
export const useGroupData = () => {
const { t } = useTranslation();
diff --git a/frontend/src/hooks/useMarketData.ts b/frontend/src/hooks/useMarketData.ts
index 6b80506..5f61d85 100644
--- a/frontend/src/hooks/useMarketData.ts
+++ b/frontend/src/hooks/useMarketData.ts
@@ -1,7 +1,7 @@
import { useState, useEffect, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { MarketServer, ApiResponse } from '@/types';
-import { getApiUrl } from '../utils/api';
+import { getApiUrl } from '../utils/runtime';
export const useMarketData = () => {
const { t } = useTranslation();
diff --git a/frontend/src/hooks/useServerData.ts b/frontend/src/hooks/useServerData.ts
index 69a24c3..49db4e6 100644
--- a/frontend/src/hooks/useServerData.ts
+++ b/frontend/src/hooks/useServerData.ts
@@ -1,7 +1,7 @@
import { useState, useEffect, useRef, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { Server, ApiResponse } from '@/types';
-import { getApiUrl } from '../utils/api';
+import { getApiUrl } from '../utils/runtime';
// Configuration options
const CONFIG = {
@@ -204,7 +204,7 @@ export const useServerData = () => {
try {
// Fetch settings to get the full server config before editing
const token = localStorage.getItem('mcphub_token');
- const response = await fetch(`/api/settings`, {
+ const response = await fetch(getApiUrl('/settings'), {
headers: {
'x-auth-token': token || '',
},
@@ -241,7 +241,7 @@ export const useServerData = () => {
const handleServerRemove = async (serverName: string) => {
try {
const token = localStorage.getItem('mcphub_token');
- const response = await fetch(`/api/servers/${serverName}`, {
+ const response = await fetch(getApiUrl(`/servers/${serverName}`), {
method: 'DELETE',
headers: {
'x-auth-token': token || '',
@@ -265,7 +265,7 @@ export const useServerData = () => {
const handleServerToggle = async (server: Server, enabled: boolean) => {
try {
const token = localStorage.getItem('mcphub_token');
- const response = await fetch(`/api/servers/${server.name}/toggle`, {
+ const response = await fetch(getApiUrl(`/servers/${server.name}/toggle`), {
method: 'POST',
headers: {
'Content-Type': 'application/json',
diff --git a/frontend/src/hooks/useSettingsData.ts b/frontend/src/hooks/useSettingsData.ts
index 0c20b3c..47565be 100644
--- a/frontend/src/hooks/useSettingsData.ts
+++ b/frontend/src/hooks/useSettingsData.ts
@@ -2,7 +2,7 @@ import { useState, useCallback, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { ApiResponse } from '@/types';
import { useToast } from '@/contexts/ToastContext';
-import { getApiUrl } from '../utils/api';
+import { getApiUrl } from '../utils/runtime';
// Define types for the settings data
interface RoutingConfig {
diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx
index 378fa92..f1cd579 100644
--- a/frontend/src/main.tsx
+++ b/frontend/src/main.tsx
@@ -1,12 +1,45 @@
-import React from 'react'
-import ReactDOM from 'react-dom/client'
-import App from './App'
-import './index.css'
+import React from 'react';
+import ReactDOM from 'react-dom/client';
+import App from './App';
+import './index.css';
// Import the i18n configuration
-import './i18n'
+import './i18n';
+import { loadRuntimeConfig } from './utils/runtime';
-ReactDOM.createRoot(document.getElementById('root')!).render(
-
-
- ,
-)
\ No newline at end of file
+// Load runtime configuration before starting the app
+async function initializeApp() {
+ try {
+ console.log('Loading runtime configuration...');
+ const config = await loadRuntimeConfig();
+ console.log('Runtime configuration loaded:', config);
+
+ // Store config in window object
+ window.__MCPHUB_CONFIG__ = config;
+
+ // Start React app
+ ReactDOM.createRoot(document.getElementById('root')!).render(
+
+
+ ,
+ );
+ } catch (error) {
+ console.error('Failed to initialize app:', error);
+
+ // Fallback: start app with default config
+ console.log('Starting app with default configuration...');
+ window.__MCPHUB_CONFIG__ = {
+ basePath: '',
+ version: 'dev',
+ name: 'mcphub',
+ };
+
+ ReactDOM.createRoot(document.getElementById('root')!).render(
+
+
+ ,
+ );
+ }
+}
+
+// Initialize the app
+initializeApp();
\ No newline at end of file
diff --git a/frontend/src/services/authService.ts b/frontend/src/services/authService.ts
index bf762df..0efa340 100644
--- a/frontend/src/services/authService.ts
+++ b/frontend/src/services/authService.ts
@@ -4,7 +4,7 @@ import {
RegisterCredentials,
ChangePasswordCredentials,
} from '../types';
-import { getApiUrl } from '../utils/api';
+import { getApiUrl } from '../utils/runtime';
// Token key in localStorage
const TOKEN_KEY = 'mcphub_token';
diff --git a/frontend/src/services/logService.ts b/frontend/src/services/logService.ts
index b4a07b6..e491227 100644
--- a/frontend/src/services/logService.ts
+++ b/frontend/src/services/logService.ts
@@ -1,6 +1,6 @@
import { useEffect, useState } from 'react';
import { getToken } from './authService'; // Import getToken function
-import { getApiUrl } from '../utils/api';
+import { getApiUrl } from '../utils/runtime';
export interface LogEntry {
timestamp: number;
diff --git a/frontend/src/types/runtime.ts b/frontend/src/types/runtime.ts
new file mode 100644
index 0000000..2d86857
--- /dev/null
+++ b/frontend/src/types/runtime.ts
@@ -0,0 +1,15 @@
+// Global runtime configuration interface
+export interface RuntimeConfig {
+ basePath: string;
+ version: string;
+ name: string;
+}
+
+// Extend Window interface to include runtime config
+declare global {
+ interface Window {
+ __MCPHUB_CONFIG__?: RuntimeConfig;
+ }
+}
+
+export {};
diff --git a/frontend/src/utils/api.ts b/frontend/src/utils/api.ts
index 5fddcd3..e111cb1 100644
--- a/frontend/src/utils/api.ts
+++ b/frontend/src/utils/api.ts
@@ -1,27 +1,28 @@
/**
* API utility functions for constructing URLs with proper base path support
+ *
+ * @deprecated Use functions from utils/runtime.ts instead for runtime configuration support
*/
+import { getApiBaseUrl as getRuntimeApiBaseUrl, getApiUrl as getRuntimeApiUrl } from './runtime';
+
/**
* Get the API base URL including base path and /api prefix
* @returns The complete API base URL
+ * @deprecated Use getApiBaseUrl from utils/runtime.ts instead
*/
export const getApiBaseUrl = (): string => {
- const basePath = import.meta.env.BASE_PATH || '';
- // Ensure the path starts with / if it's not empty and doesn't already start with /
- const normalizedBasePath = basePath && !basePath.startsWith('/') ? '/' + basePath : basePath;
- // Always append /api to the base path for API endpoints
- return normalizedBasePath + '/api';
+ console.warn('getApiBaseUrl from utils/api.ts is deprecated, use utils/runtime.ts instead');
+ return getRuntimeApiBaseUrl();
};
/**
* Construct a full API URL with the given endpoint
* @param endpoint - The API endpoint (should start with /, e.g., '/auth/login')
* @returns The complete API URL
+ * @deprecated Use getApiUrl from utils/runtime.ts instead
*/
export const getApiUrl = (endpoint: string): string => {
- const baseUrl = getApiBaseUrl();
- // Ensure endpoint starts with /
- const normalizedEndpoint = endpoint.startsWith('/') ? endpoint : '/' + endpoint;
- return baseUrl + normalizedEndpoint;
+ console.warn('getApiUrl from utils/api.ts is deprecated, use utils/runtime.ts instead');
+ return getRuntimeApiUrl(endpoint);
};
diff --git a/frontend/src/utils/runtime.ts b/frontend/src/utils/runtime.ts
new file mode 100644
index 0000000..c3ea4e7
--- /dev/null
+++ b/frontend/src/utils/runtime.ts
@@ -0,0 +1,105 @@
+import type { RuntimeConfig } from '../types/runtime';
+
+/**
+ * Get runtime configuration from window object
+ */
+export const getRuntimeConfig = (): RuntimeConfig => {
+ return (
+ window.__MCPHUB_CONFIG__ || {
+ basePath: '',
+ version: 'dev',
+ name: 'mcphub',
+ }
+ );
+};
+
+/**
+ * Get the base path from runtime configuration
+ */
+export const getBasePath = (): string => {
+ const config = getRuntimeConfig();
+ const basePath = config.basePath || '';
+
+ // Ensure the path starts with / if it's not empty and doesn't already start with /
+ if (basePath && !basePath.startsWith('/')) {
+ return '/' + basePath;
+ }
+ return basePath;
+};
+
+/**
+ * Get the API base URL including base path and /api prefix
+ */
+export const getApiBaseUrl = (): string => {
+ const basePath = getBasePath();
+ // Always append /api to the base path for API endpoints
+ return basePath + '/api';
+};
+
+/**
+ * Construct a full API URL with the given endpoint
+ */
+export const getApiUrl = (endpoint: string): string => {
+ const baseUrl = getApiBaseUrl();
+ // Ensure endpoint starts with /
+ const normalizedEndpoint = endpoint.startsWith('/') ? endpoint : '/' + endpoint;
+ return baseUrl + normalizedEndpoint;
+};
+
+/**
+ * Load runtime configuration from server
+ */
+export const loadRuntimeConfig = async (): Promise => {
+ try {
+ // For initial config load, we need to determine the correct path
+ // Try different possible paths based on current location
+ const currentPath = window.location.pathname;
+ const possibleConfigPaths = [
+ // If we're already on a subpath, try to use it
+ currentPath.replace(/\/[^\/]*$/, '') + '/config',
+ // Try root config
+ '/config',
+ // Try with potential base paths
+ ...(currentPath.includes('/')
+ ? [currentPath.split('/')[1] ? `/${currentPath.split('/')[1]}/config` : '/config']
+ : ['/config']),
+ ];
+
+ for (const configPath of possibleConfigPaths) {
+ try {
+ const response = await fetch(configPath, {
+ method: 'GET',
+ headers: {
+ Accept: 'application/json',
+ 'Cache-Control': 'no-cache',
+ },
+ });
+
+ if (response.ok) {
+ const data = await response.json();
+ if (data.success && data.data) {
+ return data.data;
+ }
+ }
+ } catch (error) {
+ // Continue to next path
+ console.debug(`Failed to load config from ${configPath}:`, error);
+ }
+ }
+
+ // Fallback to default config
+ console.warn('Could not load runtime config from server, using defaults');
+ return {
+ basePath: '',
+ version: 'dev',
+ name: 'mcphub',
+ };
+ } catch (error) {
+ console.error('Error loading runtime config:', error);
+ return {
+ basePath: '',
+ version: 'dev',
+ name: 'mcphub',
+ };
+ }
+};
diff --git a/frontend/src/vite-env.d.ts b/frontend/src/vite-env.d.ts
index e58d261..07e677c 100644
--- a/frontend/src/vite-env.d.ts
+++ b/frontend/src/vite-env.d.ts
@@ -3,7 +3,6 @@
interface ImportMeta {
readonly env: {
readonly PACKAGE_VERSION: string;
- readonly BASE_PATH?: string; // Add base path environment variable
// Add other custom env variables here if needed
[key: string]: any;
};
diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts
index 013c921..55720f9 100644
--- a/frontend/vite.config.ts
+++ b/frontend/vite.config.ts
@@ -8,12 +8,13 @@ import { readFileSync } from 'fs';
// Get package.json version
const packageJson = JSON.parse(readFileSync(path.resolve(__dirname, '../package.json'), 'utf-8'));
-// Get base path from environment variable
-const basePath = process.env.BASE_PATH || '';
+// For runtime configuration, we'll always use relative paths
+// BASE_PATH will be determined at runtime
+const basePath = '';
// https://vitejs.dev/config/
export default defineConfig({
- base: basePath || './', // Use base path or relative paths for assets
+ base: './', // Always use relative paths for runtime configuration
plugins: [react(), tailwindcss()],
resolve: {
alias: {
@@ -21,9 +22,9 @@ export default defineConfig({
},
},
define: {
- // Make package version and base path available as global variables
+ // Make package version available as global variable
+ // BASE_PATH will be loaded at runtime
'import.meta.env.PACKAGE_VERSION': JSON.stringify(packageJson.version),
- 'import.meta.env.BASE_PATH': JSON.stringify(basePath),
},
build: {
sourcemap: true, // Enable source maps for production build
diff --git a/src/controllers/configController.ts b/src/controllers/configController.ts
new file mode 100644
index 0000000..b7a1331
--- /dev/null
+++ b/src/controllers/configController.ts
@@ -0,0 +1,30 @@
+import { Request, Response } from 'express';
+import config from '../config/index.js';
+
+/**
+ * Get runtime configuration for frontend
+ */
+export const getRuntimeConfig = (req: Request, res: Response): void => {
+ try {
+ const runtimeConfig = {
+ basePath: config.basePath,
+ version: config.mcpHubVersion,
+ name: config.mcpHubName,
+ };
+
+ res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
+ res.setHeader('Pragma', 'no-cache');
+ res.setHeader('Expires', '0');
+
+ res.json({
+ success: true,
+ data: runtimeConfig,
+ });
+ } catch (error) {
+ console.error('Error getting runtime config:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Failed to get runtime configuration',
+ });
+ }
+};
diff --git a/src/routes/index.ts b/src/routes/index.ts
index 9e29487..60f6ca8 100644
--- a/src/routes/index.ts
+++ b/src/routes/index.ts
@@ -32,6 +32,7 @@ import {
} from '../controllers/marketController.js';
import { login, register, getCurrentUser, changePassword } from '../controllers/authController.js';
import { getAllLogs, clearLogs, streamLogs } from '../controllers/logController.js';
+import { getRuntimeConfig } from '../controllers/configController.js';
import { auth } from '../middlewares/auth.js';
const router = express.Router();
@@ -104,6 +105,9 @@ export const initRoutes = (app: express.Application): void => {
changePassword,
);
+ // Runtime configuration endpoint (no auth required for frontend initialization)
+ app.get(`${config.basePath}/config`, getRuntimeConfig);
+
app.use(`${config.basePath}/api`, router);
};