Compare commits

...

8 Commits

Author SHA1 Message Date
copilot-swe-agent[bot]
017e405c41 Address code review comments: remove redundant cleanup and clarify test intent
Co-authored-by: samanhappy <2755122+samanhappy@users.noreply.github.com>
2025-11-05 11:15:19 +00:00
copilot-swe-agent[bot]
8da0323326 Add BASE_PATH configuration documentation
Co-authored-by: samanhappy <2755122+samanhappy@users.noreply.github.com>
2025-11-05 11:11:22 +00:00
copilot-swe-agent[bot]
71e217fcc2 Fix BASE_PATH configuration breaking login in development mode
- Normalize BASE_PATH by removing trailing slashes in backend config
- Update Vite dev server proxy to dynamically support BASE_PATH from env
- Add integration tests for BASE_PATH functionality
- All tests pass, build successful

Co-authored-by: samanhappy <2755122+samanhappy@users.noreply.github.com>
2025-11-05 11:07:27 +00:00
copilot-swe-agent[bot]
fa133e21b0 Initial plan 2025-11-05 10:45:45 +00:00
samanhappy
602b5cb80e fix: update GitHub repository links to point to the new repository (#423) 2025-11-03 17:04:49 +08:00
samanhappy
e63f045819 refactor: remove outdated references to MCP protocol and cloud deployment in documentation (#422) 2025-11-03 17:02:10 +08:00
Chengwei Guo
a4e4791b60 fix the deployment on kubernetes (#417) 2025-11-03 14:16:12 +08:00
samanhappy
01370ea959 Revert "Feat: Enhance package cache for stdio servers (#400)" (#418) 2025-11-03 13:35:24 +08:00
13 changed files with 409 additions and 234 deletions

View File

@@ -9,25 +9,9 @@ RUN apt-get update && apt-get install -y curl gnupg git \
RUN npm install -g pnpm
ENV MCP_DATA_DIR=/app/data
ENV MCP_SERVERS_DIR=$MCP_DATA_DIR/servers
ENV MCP_NPM_DIR=$MCP_SERVERS_DIR/npm
ENV MCP_PYTHON_DIR=$MCP_SERVERS_DIR/python
ENV PNPM_HOME=$MCP_DATA_DIR/pnpm
ENV NPM_CONFIG_PREFIX=$MCP_DATA_DIR/npm-global
ENV NPM_CONFIG_CACHE=$MCP_DATA_DIR/npm-cache
ENV UV_TOOL_DIR=$MCP_DATA_DIR/uv/tools
ENV UV_CACHE_DIR=$MCP_DATA_DIR/uv/cache
ENV PATH=$PNPM_HOME:$NPM_CONFIG_PREFIX/bin:$UV_TOOL_DIR/bin:$PATH
RUN mkdir -p \
$PNPM_HOME \
$NPM_CONFIG_PREFIX/bin \
$NPM_CONFIG_PREFIX/lib/node_modules \
$NPM_CONFIG_CACHE \
$UV_TOOL_DIR \
$UV_CACHE_DIR \
$MCP_NPM_DIR \
$MCP_PYTHON_DIR && \
ENV PNPM_HOME=/usr/local/share/pnpm
ENV PATH=$PNPM_HOME:$PATH
RUN mkdir -p $PNPM_HOME && \
pnpm add -g @amap/amap-maps-mcp-server @playwright/mcp@latest tavily-mcp@latest @modelcontextprotocol/server-github @modelcontextprotocol/server-slack
ARG INSTALL_EXT=false

View File

@@ -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

View File

@@ -78,7 +78,7 @@ git clone https://github.com/YOUR_USERNAME/mcphub.git
cd mcphub
# 2. Add upstream remote
git remote add upstream https://github.com/mcphub/mcphub.git
git remote add upstream https://github.com/samanhappy/mcphub.git
# 3. Install dependencies
pnpm install

View File

@@ -294,22 +294,47 @@ Optional for Smart Routing:
labels:
app: mcphub
spec:
initContainers:
- name: prepare-config
image: busybox:1.28
command:
[
"sh",
"-c",
"cp /config-ro/mcp_settings.json /etc/mcphub/mcp_settings.json",
]
volumeMounts:
- name: config
mountPath: /config-ro
readOnly: true
- name: app-storage
mountPath: /etc/mcphub
containers:
- name: mcphub
image: samanhappy/mcphub:latest
ports:
- containerPort: 3000
env:
- name: PORT
value: "3000"
volumeMounts:
- name: config
mountPath: /app/mcp_settings.json
subPath: mcp_settings.json
- name: mcphub
image: samanhappy/mcphub:latest
ports:
- containerPort: 3000
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 512Mi
env:
- name: PORT
value: "3000"
- name: MCPHUB_SETTING_PATH
value: /etc/mcphub/mcp_settings.json
volumeMounts:
- name: app-storage
mountPath: /etc/mcphub
volumes:
- name: config
configMap:
name: mcphub-config
- name: config
configMap:
name: mcphub-config
- name: app-storage
emptyDir: {}
```
#### 3. Service

View File

@@ -48,7 +48,7 @@ docker --version
```bash
# 克隆主仓库
git clone https://github.com/mcphub/mcphub.git
git clone https://github.com/samanhappy/mcphub.git
cd mcphub
# 或者克隆您的 fork

View File

@@ -388,7 +388,7 @@ CMD ["node", "dist/index.js"]
````md
```bash
# 克隆 MCPHub 仓库
git clone https://github.com/mcphub/mcphub.git
git clone https://github.com/samanhappy/mcphub.git
cd mcphub
# 安装依赖
@@ -413,7 +413,7 @@ npm start
```bash
# 克隆 MCPHub 仓库
git clone https://github.com/mcphub/mcphub.git
git clone https://github.com/samanhappy/mcphub.git
cd mcphub
# 安装依赖
@@ -441,7 +441,7 @@ npm start
```powershell
# Windows PowerShell 安装步骤
# 克隆仓库
git clone https://github.com/mcphub/mcphub.git
git clone https://github.com/samanhappy/mcphub.git
Set-Location mcphub
# 安装 Node.js 依赖
@@ -458,7 +458,7 @@ npm run dev
```powershell
# Windows PowerShell 安装步骤
# 克隆仓库
git clone https://github.com/mcphub/mcphub.git
git clone https://github.com/samanhappy/mcphub.git
Set-Location mcphub
# 安装 Node.js 依赖

View File

@@ -331,7 +331,7 @@ MCPHub 文档支持以下图标库的图标:
"pages": [
{
"name": "GitHub 仓库",
"url": "https://github.com/mcphub/mcphub",
"url": "https://github.com/samanhappy/mcphub",
"icon": "github"
},
{
@@ -382,7 +382,6 @@ zh/
"pages": [
"zh/concepts/introduction",
"zh/concepts/architecture",
"zh/concepts/mcp-protocol",
"zh/concepts/routing"
]
}

View File

@@ -35,9 +35,6 @@ MCPHub 是一个现代化的 Model Context Protocol (MCP) 服务器管理平台
了解 MCPHub 的核心概念,为深入使用做好准备。
<CardGroup cols={2}>
<Card title="MCP 协议介绍" icon="network-wired" href="/zh/concepts/mcp-protocol">
深入了解 Model Context Protocol 的工作原理和最佳实践
</Card>
<Card title="智能路由机制" icon="route" href="/zh/features/smart-routing">
学习 MCPHub 的智能路由算法和配置策略
</Card>
@@ -57,12 +54,6 @@ MCPHub 支持多种部署方式,满足不同规模和场景的需求。
<Card title="Docker 部署" icon="docker" href="/zh/configuration/docker-setup">
使用 Docker 容器快速部署,支持单机和集群模式
</Card>
<Card title="云服务部署" icon="cloud" href="/zh/deployment/cloud">
在 AWS、GCP、Azure 等云平台上部署 MCPHub
</Card>
<Card title="Kubernetes" icon="dharmachakra" href="/zh/deployment/kubernetes">
在 Kubernetes 集群中部署高可用的 MCPHub 服务
</Card>
</CardGroup>
## API 和集成
@@ -73,9 +64,6 @@ MCPHub 提供完整的 RESTful API 和多语言 SDK方便与现有系统集
<Card title="API 参考文档" icon="code" href="/zh/api-reference/introduction">
完整的 API 接口文档,包含详细的请求示例和响应格式
</Card>
<Card title="SDK 和工具" icon="toolbox" href="/zh/sdk">
官方 SDK 和命令行工具,加速开发集成
</Card>
</CardGroup>
## 社区和支持
@@ -83,7 +71,7 @@ MCPHub 提供完整的 RESTful API 和多语言 SDK方便与现有系统集
加入 MCPHub 社区,获取帮助和分享经验。
<CardGroup cols={2}>
<Card title="GitHub 仓库" icon="github" href="https://github.com/mcphub/mcphub">
<Card title="GitHub 仓库" icon="github" href="https://github.com/samanhappy/mcphub">
查看源代码、提交问题和贡献代码
</Card>
<Card title="Discord 社区" icon="discord" href="https://discord.gg/mcphub">

View File

@@ -1,27 +1,5 @@
#!/bin/bash
DATA_DIR=${MCP_DATA_DIR:-/app/data}
SERVERS_DIR=${MCP_SERVERS_DIR:-$DATA_DIR/servers}
NPM_SERVER_DIR=${MCP_NPM_DIR:-$SERVERS_DIR/npm}
PYTHON_SERVER_DIR=${MCP_PYTHON_DIR:-$SERVERS_DIR/python}
PNPM_HOME=${PNPM_HOME:-$DATA_DIR/pnpm}
NPM_CONFIG_PREFIX=${NPM_CONFIG_PREFIX:-$DATA_DIR/npm-global}
NPM_CONFIG_CACHE=${NPM_CONFIG_CACHE:-$DATA_DIR/npm-cache}
UV_TOOL_DIR=${UV_TOOL_DIR:-$DATA_DIR/uv/tools}
UV_CACHE_DIR=${UV_CACHE_DIR:-$DATA_DIR/uv/cache}
mkdir -p \
"$PNPM_HOME" \
"$NPM_CONFIG_PREFIX/bin" \
"$NPM_CONFIG_PREFIX/lib/node_modules" \
"$NPM_CONFIG_CACHE" \
"$UV_TOOL_DIR" \
"$UV_CACHE_DIR" \
"$NPM_SERVER_DIR" \
"$PYTHON_SERVER_DIR"
export PATH="$PNPM_HOME:$NPM_CONFIG_PREFIX/bin:$UV_TOOL_DIR/bin:$PATH"
NPM_REGISTRY=${NPM_REGISTRY:-https://registry.npmjs.org/}
echo "Setting npm registry to ${NPM_REGISTRY}"
npm config set registry "$NPM_REGISTRY"

View File

@@ -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';
@@ -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,
},
};
});

View File

@@ -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(),

View File

@@ -1,6 +1,4 @@
import fs from 'fs';
import os from 'os';
import path from 'path';
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import {
CallToolRequestSchema,
@@ -33,77 +31,6 @@ const servers: { [sessionId: string]: Server } = {};
const serverDao = getServerDao();
const ensureDirExists = (dir: string | undefined): string => {
if (!dir) {
throw new Error('Directory path is undefined');
}
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
return dir;
};
const getDataRootDir = (): string => {
return ensureDirExists(process.env.MCP_DATA_DIR || path.join(process.cwd(), 'data'));
};
const getServersStorageRoot = (): string => {
return ensureDirExists(process.env.MCP_SERVERS_DIR || path.join(getDataRootDir(), 'servers'));
};
const getNpmBaseDir = (): string => {
return ensureDirExists(process.env.MCP_NPM_DIR || path.join(getServersStorageRoot(), 'npm'));
};
const getPythonBaseDir = (): string => {
return ensureDirExists(
process.env.MCP_PYTHON_DIR || path.join(getServersStorageRoot(), 'python'),
);
};
const getNpmCacheDir = (): string => {
return ensureDirExists(process.env.NPM_CONFIG_CACHE || path.join(getDataRootDir(), 'npm-cache'));
};
const getNpmPrefixDir = (): string => {
const dir = ensureDirExists(
process.env.NPM_CONFIG_PREFIX || path.join(getDataRootDir(), 'npm-global'),
);
ensureDirExists(path.join(dir, 'bin'));
ensureDirExists(path.join(dir, 'lib', 'node_modules'));
return dir;
};
const getUvCacheDir = (): string => {
return ensureDirExists(process.env.UV_CACHE_DIR || path.join(getDataRootDir(), 'uv', 'cache'));
};
const getUvToolDir = (): string => {
const dir = ensureDirExists(process.env.UV_TOOL_DIR || path.join(getDataRootDir(), 'uv', 'tools'));
ensureDirExists(path.join(dir, 'bin'));
return dir;
};
const getServerInstallDir = (serverName: string, kind: 'npm' | 'python'): string => {
const baseDir = kind === 'npm' ? getNpmBaseDir() : getPythonBaseDir();
return ensureDirExists(path.join(baseDir, serverName));
};
const prependToPath = (currentPath: string, dir: string): string => {
if (!dir) {
return currentPath;
}
const delimiter = path.delimiter;
const segments = currentPath ? currentPath.split(delimiter) : [];
if (segments.includes(dir)) {
return currentPath;
}
return currentPath ? `${dir}${delimiter}${currentPath}` : dir;
};
const NODE_COMMANDS = new Set(['npm', 'npx', 'pnpm', 'yarn', 'node', 'bun', 'bunx']);
const PYTHON_COMMANDS = new Set(['uv', 'uvx', 'python', 'pip', 'pip3', 'pipx']);
// Helper function to set up keep-alive ping for SSE connections
const setupKeepAlive = (serverInfo: ServerInfo, serverConfig: ServerConfig): void => {
// Only set up keep-alive for SSE connections
@@ -286,7 +213,7 @@ export const createTransportFromConfig = async (name: string, conf: ServerConfig
...(process.env as Record<string, string>),
...replaceEnvVars(conf.env || {}),
};
env['PATH'] = expandEnvVars(env['PATH'] || process.env.PATH || '');
env['PATH'] = expandEnvVars(process.env.PATH as string) || '';
const settings = loadSettings();
// Add UV_DEFAULT_INDEX and npm_config_registry if needed
@@ -308,52 +235,9 @@ export const createTransportFromConfig = async (name: string, conf: ServerConfig
env['npm_config_registry'] = settings.systemConfig.install.npmRegistry;
}
// Ensure stdio servers use persistent directories under /app/data (or configured override)
let workingDirectory = os.homedir();
const commandLower = conf.command.toLowerCase();
if (NODE_COMMANDS.has(commandLower)) {
const serverDir = getServerInstallDir(name, 'npm');
workingDirectory = serverDir;
const npmCacheDir = getNpmCacheDir();
const npmPrefixDir = getNpmPrefixDir();
if (!env['npm_config_cache']) {
env['npm_config_cache'] = npmCacheDir;
}
if (!env['NPM_CONFIG_CACHE']) {
env['NPM_CONFIG_CACHE'] = env['npm_config_cache'];
}
if (!env['npm_config_prefix']) {
env['npm_config_prefix'] = npmPrefixDir;
}
if (!env['NPM_CONFIG_PREFIX']) {
env['NPM_CONFIG_PREFIX'] = env['npm_config_prefix'];
}
env['PATH'] = prependToPath(env['PATH'], path.join(env['npm_config_prefix'], 'bin'));
} else if (PYTHON_COMMANDS.has(commandLower)) {
const serverDir = getServerInstallDir(name, 'python');
workingDirectory = serverDir;
const uvCacheDir = getUvCacheDir();
const uvToolDir = getUvToolDir();
if (!env['UV_CACHE_DIR']) {
env['UV_CACHE_DIR'] = uvCacheDir;
}
if (!env['UV_TOOL_DIR']) {
env['UV_TOOL_DIR'] = uvToolDir;
}
env['PATH'] = prependToPath(env['PATH'], path.join(env['UV_TOOL_DIR'], 'bin'));
}
// Expand environment variables in command
transport = new StdioClientTransport({
cwd: workingDirectory,
cwd: os.homedir(),
command: conf.command,
args: replaceEnvVars(conf.args) as string[],
env: env,

View File

@@ -0,0 +1,130 @@
import { beforeEach, describe, expect, it, jest } from '@jest/globals';
import request from 'supertest';
// Mock dependencies
jest.mock('../../src/utils/i18n.js', () => ({
__esModule: true,
initI18n: jest.fn().mockResolvedValue(undefined),
}));
jest.mock('../../src/models/User.js', () => ({
__esModule: true,
initializeDefaultUser: jest.fn().mockResolvedValue(undefined),
}));
jest.mock('../../src/services/oauthService.js', () => ({
__esModule: true,
initOAuthProvider: jest.fn(),
getOAuthRouter: jest.fn(() => null),
}));
jest.mock('../../src/services/mcpService.js', () => ({
__esModule: true,
initUpstreamServers: jest.fn().mockResolvedValue(undefined),
connected: jest.fn().mockReturnValue(true),
}));
jest.mock('../../src/middlewares/userContext.js', () => ({
__esModule: true,
userContextMiddleware: jest.fn((_req, _res, next) => next()),
sseUserContextMiddleware: jest.fn((_req, _res, next) => next()),
}));
describe('AppServer with BASE_PATH configuration', () => {
// Save original BASE_PATH
const originalBasePath = process.env.BASE_PATH;
beforeEach(() => {
jest.clearAllMocks();
// Clear module cache to allow fresh imports with different config
jest.resetModules();
});
afterEach(() => {
// Restore original BASE_PATH or remove it
if (originalBasePath !== undefined) {
process.env.BASE_PATH = originalBasePath;
} else {
delete process.env.BASE_PATH;
}
});
const flushPromises = async () => {
await new Promise((resolve) => setImmediate(resolve));
};
it('should serve auth routes with BASE_PATH=/mcphub/', async () => {
// Set environment variable for BASE_PATH (with trailing slash)
process.env.BASE_PATH = '/mcphub/';
// Dynamically import after setting env var
const { AppServer } = await import('../../src/server.js');
const config = await import('../../src/config/index.js');
// Verify config loaded the BASE_PATH and normalized it (removed trailing slash)
expect(config.default.basePath).toBe('/mcphub');
const appServer = new AppServer();
await appServer.initialize();
await flushPromises();
const app = appServer.getApp();
// Test that /mcphub/config endpoint exists
const configResponse = await request(app).get('/mcphub/config');
expect(configResponse.status).not.toBe(404);
// Test that /mcphub/public-config endpoint exists
const publicConfigResponse = await request(app).get('/mcphub/public-config');
expect(publicConfigResponse.status).not.toBe(404);
});
it('should serve auth routes without BASE_PATH (default)', async () => {
// Ensure BASE_PATH is not set
delete process.env.BASE_PATH;
// Dynamically import after clearing env var
jest.resetModules();
const { AppServer } = await import('../../src/server.js');
const config = await import('../../src/config/index.js');
// Verify config has empty BASE_PATH
expect(config.default.basePath).toBe('');
const appServer = new AppServer();
await appServer.initialize();
await flushPromises();
const app = appServer.getApp();
// Test that /config endpoint exists (without base path)
const configResponse = await request(app).get('/config');
expect(configResponse.status).not.toBe(404);
// Test that /public-config endpoint exists
const publicConfigResponse = await request(app).get('/public-config');
expect(publicConfigResponse.status).not.toBe(404);
});
it('should serve global endpoints without BASE_PATH prefix', async () => {
process.env.BASE_PATH = '/test-base/';
jest.resetModules();
const { AppServer } = await import('../../src/server.js');
const appServer = new AppServer();
await appServer.initialize();
await flushPromises();
const app = appServer.getApp();
// Test that /health endpoint is accessible globally (no BASE_PATH prefix)
// The /health endpoint is intentionally mounted without BASE_PATH
const healthResponse = await request(app).get('/health');
expect(healthResponse.status).not.toBe(404);
// Also verify that BASE_PATH prefixed routes exist
const configResponse = await request(app).get('/test-base/config');
expect(configResponse.status).not.toBe(404);
});
});