Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
175 changes: 175 additions & 0 deletions docs/BASE_PATH_CONFIGURATION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
# BASE_PATH Configuration Guide

## Overview

MCPHub supports running under a custom base path (e.g., `/mcphub/`) for scenarios where you need to deploy the application under a subdirectory or behind a reverse proxy.

## Configuration

### Setting BASE_PATH

Add the `BASE_PATH` environment variable to your `.env` file:

```bash
PORT=3000
NODE_ENV=development
BASE_PATH=/mcphub/
```

**Note:** Trailing slashes in BASE_PATH are automatically normalized (removed). Both `/mcphub/` and `/mcphub` will work and be normalized to `/mcphub`.

### In Production (Docker)

Set the environment variable when running the container:

```bash
docker run -e BASE_PATH=/mcphub/ -p 3000:3000 mcphub
```

### Behind a Reverse Proxy (nginx)

Example nginx configuration:

```nginx
location /mcphub/ {
proxy_pass http://localhost:3000/mcphub/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
}
```

## How It Works

### Backend Routes

All backend routes are automatically prefixed with BASE_PATH:

- **Without BASE_PATH:**
- Config: `http://localhost:3000/config`
- Auth: `http://localhost:3000/api/auth/login`
- Health: `http://localhost:3000/health`

- **With BASE_PATH="/mcphub":**
- Config: `http://localhost:3000/mcphub/config`
- Auth: `http://localhost:3000/mcphub/api/auth/login`
- Health: `http://localhost:3000/health` (global, no prefix)

### Frontend

The frontend automatically detects the BASE_PATH at runtime by calling the `/config` endpoint. All API calls are automatically prefixed.

### Development Mode

The Vite dev server proxy is automatically configured to support BASE_PATH:

1. Set `BASE_PATH` in your `.env` file
2. Start the dev server: `pnpm dev`
3. Access the application through Vite: `http://localhost:5173`
4. All API calls are proxied correctly with the BASE_PATH prefix

## Testing

You can test the BASE_PATH configuration with curl:

```bash
# Set BASE_PATH=/mcphub/ in .env file

# Test config endpoint
curl http://localhost:3000/mcphub/config

# Test login
curl -X POST http://localhost:3000/mcphub/api/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"admin123"}'
```

## Troubleshooting

### Issue: Login fails with BASE_PATH set

**Solution:** Make sure you're using version 0.10.4 or later, which includes the fix for BASE_PATH in development mode.

### Issue: 404 errors on API endpoints

**Symptoms:**
- Login returns 404
- Config endpoint returns 404
- API calls fail with 404

**Solution:**
1. Verify BASE_PATH is set correctly in `.env` file
2. Restart the backend server to pick up the new configuration
3. Check that you're accessing the correct URL with the BASE_PATH prefix

### Issue: Vite proxy not working

**Solution:**
1. Ensure you have the latest version of `frontend/vite.config.ts`
2. Restart the frontend dev server
3. Verify the BASE_PATH is being loaded from the `.env` file in the project root

## Implementation Details

### Backend (src/config/index.ts)

```typescript
const normalizeBasePath = (path: string): string => {
if (!path) return '';
return path.replace(/\/+$/, '');
};

const defaultConfig = {
basePath: normalizeBasePath(process.env.BASE_PATH || ''),
// ...
};
```

### Frontend (frontend/vite.config.ts)

```typescript
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, path.resolve(__dirname, '..'), '');
let basePath = env.BASE_PATH || '';
basePath = basePath.replace(/\/+$/, '');

const proxyConfig: Record<string, any> = {};
const pathsToProxy = ['/api', '/config', '/public-config', '/health', '/oauth'];

pathsToProxy.forEach((path) => {
const proxyPath = basePath + path;
proxyConfig[proxyPath] = {
target: 'http://localhost:3000',
changeOrigin: true,
};
});

return {
server: {
proxy: proxyConfig,
},
};
});
```

### Frontend Runtime (frontend/src/utils/runtime.ts)

The frontend loads the BASE_PATH at runtime from the `/config` endpoint:

```typescript
export const loadRuntimeConfig = async (): Promise<RuntimeConfig> => {
// Tries different possible config paths
const response = await fetch('/config');
const data = await response.json();
return data.data; // Contains basePath, version, name
};
```

## Related Files

- `src/config/index.ts` - Backend BASE_PATH normalization
- `frontend/vite.config.ts` - Vite proxy configuration
- `frontend/src/utils/runtime.ts` - Frontend runtime config loading
- `tests/integration/base-path-routes.test.ts` - Integration tests
81 changes: 42 additions & 39 deletions frontend/vite.config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { defineConfig } from 'vite';
import { defineConfig, loadEnv } from 'vite';
import react from '@vitejs/plugin-react';
import path from 'path';
import tailwindcss from '@tailwindcss/vite';
Expand All @@ -8,45 +8,48 @@ import { readFileSync } from 'fs';
// Get package.json version
const packageJson = JSON.parse(readFileSync(path.resolve(__dirname, '../package.json'), 'utf-8'));

// 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: './', // Always use relative paths for runtime configuration
plugins: [react(), tailwindcss()],
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
define: {
// Make package version available as global variable
// BASE_PATH will be loaded at runtime
'import.meta.env.PACKAGE_VERSION': JSON.stringify(packageJson.version),
},
build: {
sourcemap: true, // Enable source maps for production build
},
server: {
proxy: {
[`${basePath}/api`]: {
target: 'http://localhost:3000',
changeOrigin: true,
},
[`${basePath}/auth`]: {
target: 'http://localhost:3000',
changeOrigin: true,
},
[`${basePath}/config`]: {
target: 'http://localhost:3000',
changeOrigin: true,
},
[`${basePath}/public-config`]: {
target: 'http://localhost:3000',
changeOrigin: true,
export default defineConfig(({ mode }) => {
// Load env file from parent directory (project root)
const env = loadEnv(mode, path.resolve(__dirname, '..'), '');

// Get BASE_PATH from environment, default to empty string
// Normalize by removing trailing slashes to avoid double slashes
let basePath = env.BASE_PATH || '';
basePath = basePath.replace(/\/+$/, '');

// Create proxy configuration dynamically based on BASE_PATH
const proxyConfig: Record<string, any> = {};

// List of paths that need to be proxied
const pathsToProxy = ['/api', '/config', '/public-config', '/health', '/oauth'];

pathsToProxy.forEach((path) => {
const proxyPath = basePath + path;
proxyConfig[proxyPath] = {
target: 'http://localhost:3000',
changeOrigin: true,
};
});

return {
base: './', // Always use relative paths for runtime configuration
plugins: [react(), tailwindcss()],
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
},
define: {
// Make package version available as global variable
// BASE_PATH will be loaded at runtime
'import.meta.env.PACKAGE_VERSION': JSON.stringify(packageJson.version),
},
build: {
sourcemap: true, // Enable source maps for production build
},
server: {
proxy: proxyConfig,
},
};
});
11 changes: 10 additions & 1 deletion src/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,19 @@ import { DataService } from '../services/dataService.js';

dotenv.config();

/**
* Normalize the base path by removing trailing slashes
*/
const normalizeBasePath = (path: string): string => {
if (!path) return '';
// Remove trailing slashes
return path.replace(/\/+$/, '');
};

const defaultConfig = {
port: process.env.PORT || 3000,
initTimeout: process.env.INIT_TIMEOUT || 300000,
basePath: process.env.BASE_PATH || '',
basePath: normalizeBasePath(process.env.BASE_PATH || ''),
readonly: 'true' === process.env.READONLY || false,
mcpHubName: 'mcphub',
mcpHubVersion: getPackageVersion(),
Expand Down
Loading