diff --git a/Dockerfile b/Dockerfile
index 84e8750..7eff4a5 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -18,8 +18,8 @@ RUN npm install -g pnpm
ARG REQUEST_TIMEOUT=60000
ENV REQUEST_TIMEOUT=$REQUEST_TIMEOUT
-ARG VITE_BASE_PATH=""
-ENV VITE_BASE_PATH=$VITE_BASE_PATH
+ARG BASE_PATH=""
+ENV BASE_PATH=$BASE_PATH
ENV PNPM_HOME=/usr/local/share/pnpm
ENV PATH=$PNPM_HOME:$PATH
diff --git a/frontend/index.html b/frontend/index.html
index e1203c4..8449b96 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -1,16 +1,13 @@
-
MCP Hub Dashboard
-
+
-
-
+
-
\ No newline at end of file
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index 2687561..84aff6a 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -15,13 +15,16 @@ import LogsPage from './pages/LogsPage';
// Get base path from environment variable or default to empty string
const getBasePath = (): string => {
- const basePath = import.meta.env.VITE_BASE_PATH || '';
- return basePath.startsWith('/') ? basePath : '';
+ 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;
};
function App() {
const basename = getBasePath();
-
return (
diff --git a/frontend/src/components/AddServerForm.tsx b/frontend/src/components/AddServerForm.tsx
index 4d84a79..4d2030d 100644
--- a/frontend/src/components/AddServerForm.tsx
+++ b/frontend/src/components/AddServerForm.tsx
@@ -1,6 +1,7 @@
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import ServerForm from './ServerForm'
+import { getApiUrl } from '../utils/api'
interface AddServerFormProps {
onAdd: () => void
@@ -20,7 +21,7 @@ const AddServerForm = ({ onAdd }: AddServerFormProps) => {
try {
setError(null)
const token = localStorage.getItem('mcphub_token');
- const response = await fetch('/api/servers', {
+ const response = await fetch(getApiUrl('/servers'), {
method: 'POST',
headers: {
'Content-Type': 'application/json',
diff --git a/frontend/src/hooks/useGroupData.ts b/frontend/src/hooks/useGroupData.ts
index 4f28286..3d48c69 100644
--- a/frontend/src/hooks/useGroupData.ts
+++ b/frontend/src/hooks/useGroupData.ts
@@ -1,6 +1,7 @@
import { useState, useEffect, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { Group, ApiResponse } from '@/types';
+import { getApiUrl } from '../utils/api';
export const useGroupData = () => {
const { t } = useTranslation();
@@ -13,25 +14,25 @@ export const useGroupData = () => {
try {
setLoading(true);
const token = localStorage.getItem('mcphub_token');
- const response = await fetch('/api/groups', {
+ const response = await fetch(getApiUrl('/groups'), {
headers: {
- 'x-auth-token': token || ''
- }
+ 'x-auth-token': token || '',
+ },
});
-
+
if (!response.ok) {
throw new Error(`Status: ${response.status}`);
}
-
+
const data: ApiResponse = await response.json();
-
+
if (data && data.success && Array.isArray(data.data)) {
setGroups(data.data);
} else {
console.error('Invalid group data format:', data);
setGroups([]);
}
-
+
setError(null);
} catch (err) {
console.error('Error fetching groups:', err);
@@ -44,29 +45,29 @@ export const useGroupData = () => {
// Trigger a refresh of the groups data
const triggerRefresh = useCallback(() => {
- setRefreshKey(prev => prev + 1);
+ setRefreshKey((prev) => prev + 1);
}, []);
// Create a new group with server associations
const createGroup = async (name: string, description?: string, servers: string[] = []) => {
try {
const token = localStorage.getItem('mcphub_token');
- const response = await fetch('/api/groups', {
+ const response = await fetch(getApiUrl('/groups'), {
method: 'POST',
headers: {
'Content-Type': 'application/json',
- 'x-auth-token': token || ''
+ 'x-auth-token': token || '',
},
body: JSON.stringify({ name, description, servers }),
});
-
+
const result: ApiResponse = await response.json();
-
+
if (!response.ok) {
setError(result.message || t('groups.createError'));
return null;
}
-
+
triggerRefresh();
return result.data || null;
} catch (err) {
@@ -76,25 +77,28 @@ export const useGroupData = () => {
};
// Update an existing group with server associations
- const updateGroup = async (id: string, data: { name?: string; description?: string; servers?: string[] }) => {
+ const updateGroup = async (
+ id: string,
+ data: { name?: string; description?: string; servers?: string[] },
+ ) => {
try {
const token = localStorage.getItem('mcphub_token');
- const response = await fetch(`/api/groups/${id}`, {
+ const response = await fetch(getApiUrl(`/groups/${id}`), {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
- 'x-auth-token': token || ''
+ 'x-auth-token': token || '',
},
body: JSON.stringify(data),
});
-
+
const result: ApiResponse = await response.json();
-
+
if (!response.ok) {
setError(result.message || t('groups.updateError'));
return null;
}
-
+
triggerRefresh();
return result.data || null;
} catch (err) {
@@ -107,22 +111,22 @@ export const useGroupData = () => {
const updateGroupServers = async (groupId: string, servers: string[]) => {
try {
const token = localStorage.getItem('mcphub_token');
- const response = await fetch(`/api/groups/${groupId}/servers/batch`, {
+ const response = await fetch(getApiUrl(`/groups/${groupId}/servers/batch`), {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
- 'x-auth-token': token || ''
+ 'x-auth-token': token || '',
},
body: JSON.stringify({ servers }),
});
-
+
const result: ApiResponse = await response.json();
-
+
if (!response.ok) {
setError(result.message || t('groups.updateError'));
return null;
}
-
+
triggerRefresh();
return result.data || null;
} catch (err) {
@@ -135,20 +139,20 @@ export const useGroupData = () => {
const deleteGroup = async (id: string) => {
try {
const token = localStorage.getItem('mcphub_token');
- const response = await fetch(`/api/groups/${id}`, {
+ const response = await fetch(getApiUrl(`/groups/${id}`), {
method: 'DELETE',
headers: {
- 'x-auth-token': token || ''
- }
+ 'x-auth-token': token || '',
+ },
});
-
+
const result = await response.json();
-
+
if (!response.ok) {
setError(result.message || t('groups.deleteError'));
return false;
}
-
+
triggerRefresh();
return true;
} catch (err) {
@@ -161,22 +165,22 @@ export const useGroupData = () => {
const addServerToGroup = async (groupId: string, serverName: string) => {
try {
const token = localStorage.getItem('mcphub_token');
- const response = await fetch(`/api/groups/${groupId}/servers`, {
+ const response = await fetch(getApiUrl(`/groups/${groupId}/servers`), {
method: 'POST',
headers: {
'Content-Type': 'application/json',
- 'x-auth-token': token || ''
+ 'x-auth-token': token || '',
},
body: JSON.stringify({ serverName }),
});
-
+
const result: ApiResponse = await response.json();
-
+
if (!response.ok) {
setError(result.message || t('groups.serverAddError'));
return null;
}
-
+
triggerRefresh();
return result.data || null;
} catch (err) {
@@ -189,20 +193,20 @@ export const useGroupData = () => {
const removeServerFromGroup = async (groupId: string, serverName: string) => {
try {
const token = localStorage.getItem('mcphub_token');
- const response = await fetch(`/api/groups/${groupId}/servers/${serverName}`, {
+ const response = await fetch(getApiUrl(`/groups/${groupId}/servers/${serverName}`), {
method: 'DELETE',
headers: {
- 'x-auth-token': token || ''
- }
+ 'x-auth-token': token || '',
+ },
});
-
+
const result: ApiResponse = await response.json();
-
+
if (!response.ok) {
setError(result.message || t('groups.serverRemoveError'));
return null;
}
-
+
triggerRefresh();
return result.data || null;
} catch (err) {
@@ -227,6 +231,6 @@ export const useGroupData = () => {
updateGroupServers,
deleteGroup,
addServerToGroup,
- removeServerFromGroup
+ removeServerFromGroup,
};
-};
\ No newline at end of file
+};
diff --git a/frontend/src/hooks/useMarketData.ts b/frontend/src/hooks/useMarketData.ts
index 32e175a..6b80506 100644
--- a/frontend/src/hooks/useMarketData.ts
+++ b/frontend/src/hooks/useMarketData.ts
@@ -1,6 +1,7 @@
import { useState, useEffect, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { MarketServer, ApiResponse } from '@/types';
+import { getApiUrl } from '../utils/api';
export const useMarketData = () => {
const { t } = useTranslation();
@@ -15,7 +16,7 @@ export const useMarketData = () => {
const [error, setError] = useState(null);
const [currentServer, setCurrentServer] = useState(null);
const [installedServers, setInstalledServers] = useState([]);
-
+
// Pagination states
const [currentPage, setCurrentPage] = useState(1);
const [serversPerPage, setServersPerPage] = useState(9);
@@ -26,18 +27,18 @@ export const useMarketData = () => {
try {
setLoading(true);
const token = localStorage.getItem('mcphub_token');
- const response = await fetch('/api/market/servers', {
+ const response = await fetch(getApiUrl('/market/servers'), {
headers: {
- 'x-auth-token': token || ''
- }
+ 'x-auth-token': token || '',
+ },
});
-
+
if (!response.ok) {
throw new Error(`Status: ${response.status}`);
}
-
+
const data: ApiResponse = await response.json();
-
+
if (data && data.success && Array.isArray(data.data)) {
setAllServers(data.data);
// Apply pagination to the fetched data
@@ -55,44 +56,50 @@ export const useMarketData = () => {
}, [t]);
// Apply pagination to data
- const applyPagination = useCallback((data: MarketServer[], page: number, itemsPerPage = serversPerPage) => {
- const totalItems = data.length;
- const calculatedTotalPages = Math.ceil(totalItems / itemsPerPage);
- setTotalPages(calculatedTotalPages);
+ const applyPagination = useCallback(
+ (data: MarketServer[], page: number, itemsPerPage = serversPerPage) => {
+ const totalItems = data.length;
+ const calculatedTotalPages = Math.ceil(totalItems / itemsPerPage);
+ setTotalPages(calculatedTotalPages);
- // Ensure current page is valid
- const validPage = Math.max(1, Math.min(page, calculatedTotalPages));
- if (validPage !== page) {
- setCurrentPage(validPage);
- }
+ // Ensure current page is valid
+ const validPage = Math.max(1, Math.min(page, calculatedTotalPages));
+ if (validPage !== page) {
+ setCurrentPage(validPage);
+ }
- const startIndex = (validPage - 1) * itemsPerPage;
- const paginatedServers = data.slice(startIndex, startIndex + itemsPerPage);
- setServers(paginatedServers);
- }, [serversPerPage]);
+ const startIndex = (validPage - 1) * itemsPerPage;
+ const paginatedServers = data.slice(startIndex, startIndex + itemsPerPage);
+ setServers(paginatedServers);
+ },
+ [serversPerPage],
+ );
// Change page
- const changePage = useCallback((page: number) => {
- setCurrentPage(page);
- applyPagination(allServers, page, serversPerPage);
- }, [allServers, applyPagination, serversPerPage]);
+ const changePage = useCallback(
+ (page: number) => {
+ setCurrentPage(page);
+ applyPagination(allServers, page, serversPerPage);
+ },
+ [allServers, applyPagination, serversPerPage],
+ );
// Fetch all categories
const fetchCategories = useCallback(async () => {
try {
const token = localStorage.getItem('mcphub_token');
- const response = await fetch('/api/market/categories', {
+ const response = await fetch(getApiUrl('/market/categories'), {
headers: {
- 'x-auth-token': token || ''
- }
+ 'x-auth-token': token || '',
+ },
});
-
+
if (!response.ok) {
throw new Error(`Status: ${response.status}`);
}
-
+
const data: ApiResponse = await response.json();
-
+
if (data && data.success && Array.isArray(data.data)) {
setCategories(data.data);
} else {
@@ -107,18 +114,18 @@ export const useMarketData = () => {
const fetchTags = useCallback(async () => {
try {
const token = localStorage.getItem('mcphub_token');
- const response = await fetch('/api/market/tags', {
+ const response = await fetch(getApiUrl('/market/tags'), {
headers: {
- 'x-auth-token': token || ''
- }
+ 'x-auth-token': token || '',
+ },
});
-
+
if (!response.ok) {
throw new Error(`Status: ${response.status}`);
}
-
+
const data: ApiResponse = await response.json();
-
+
if (data && data.success && Array.isArray(data.data)) {
setTags(data.data);
} else {
@@ -130,178 +137,196 @@ export const useMarketData = () => {
}, []);
// Fetch server by name
- const fetchServerByName = useCallback(async (name: string) => {
- try {
- setLoading(true);
- const token = localStorage.getItem('mcphub_token');
- const response = await fetch(`/api/market/servers/${name}`, {
- headers: {
- 'x-auth-token': token || ''
+ const fetchServerByName = useCallback(
+ async (name: string) => {
+ try {
+ setLoading(true);
+ const token = localStorage.getItem('mcphub_token');
+ const response = await fetch(getApiUrl(`/market/servers/${name}`), {
+ headers: {
+ 'x-auth-token': token || '',
+ },
+ });
+
+ if (!response.ok) {
+ throw new Error(`Status: ${response.status}`);
}
- });
-
- if (!response.ok) {
- throw new Error(`Status: ${response.status}`);
- }
-
- const data: ApiResponse = await response.json();
-
- if (data && data.success && data.data) {
- setCurrentServer(data.data);
- return data.data;
- } else {
- console.error('Invalid server data format:', data);
- setError(t('market.serverNotFound'));
+
+ const data: ApiResponse = await response.json();
+
+ if (data && data.success && data.data) {
+ setCurrentServer(data.data);
+ return data.data;
+ } else {
+ console.error('Invalid server data format:', data);
+ setError(t('market.serverNotFound'));
+ return null;
+ }
+ } catch (err) {
+ console.error(`Error fetching server ${name}:`, err);
+ setError(err instanceof Error ? err.message : String(err));
return null;
+ } finally {
+ setLoading(false);
}
- } catch (err) {
- console.error(`Error fetching server ${name}:`, err);
- setError(err instanceof Error ? err.message : String(err));
- return null;
- } finally {
- setLoading(false);
- }
- }, [t]);
+ },
+ [t],
+ );
// Search servers by query
- const searchServers = useCallback(async (query: string) => {
- try {
- setLoading(true);
- setSearchQuery(query);
-
- if (!query.trim()) {
- // Fetch fresh data from server instead of just applying pagination
- fetchMarketServers();
- return;
- }
-
- const token = localStorage.getItem('mcphub_token');
- const response = await fetch(`/api/market/servers/search?query=${encodeURIComponent(query)}`, {
- headers: {
- 'x-auth-token': token || ''
+ const searchServers = useCallback(
+ async (query: string) => {
+ try {
+ setLoading(true);
+ setSearchQuery(query);
+
+ if (!query.trim()) {
+ // Fetch fresh data from server instead of just applying pagination
+ fetchMarketServers();
+ return;
}
- });
-
- if (!response.ok) {
- throw new Error(`Status: ${response.status}`);
+
+ const token = localStorage.getItem('mcphub_token');
+ const response = await fetch(
+ getApiUrl(`/market/servers/search?query=${encodeURIComponent(query)}`),
+ {
+ headers: {
+ 'x-auth-token': token || '',
+ },
+ },
+ );
+
+ if (!response.ok) {
+ throw new Error(`Status: ${response.status}`);
+ }
+
+ const data: ApiResponse = await response.json();
+
+ if (data && data.success && Array.isArray(data.data)) {
+ setAllServers(data.data);
+ setCurrentPage(1);
+ applyPagination(data.data, 1);
+ } else {
+ console.error('Invalid search results format:', data);
+ setError(t('market.searchError'));
+ }
+ } catch (err) {
+ console.error('Error searching servers:', err);
+ setError(err instanceof Error ? err.message : String(err));
+ } finally {
+ setLoading(false);
}
-
- const data: ApiResponse = await response.json();
-
- if (data && data.success && Array.isArray(data.data)) {
- setAllServers(data.data);
- setCurrentPage(1);
- applyPagination(data.data, 1);
- } else {
- console.error('Invalid search results format:', data);
- setError(t('market.searchError'));
- }
- } catch (err) {
- console.error('Error searching servers:', err);
- setError(err instanceof Error ? err.message : String(err));
- } finally {
- setLoading(false);
- }
- }, [t, allServers, applyPagination, fetchMarketServers]);
+ },
+ [t, allServers, applyPagination, fetchMarketServers],
+ );
// Filter servers by category
- const filterByCategory = useCallback(async (category: string) => {
- try {
- setLoading(true);
- setSelectedCategory(category);
- setSelectedTag(''); // Reset tag filter when filtering by category
-
- if (!category) {
- fetchMarketServers();
- return;
- }
-
- const token = localStorage.getItem('mcphub_token');
- const response = await fetch(`/api/market/categories/${encodeURIComponent(category)}`, {
- headers: {
- 'x-auth-token': token || ''
+ const filterByCategory = useCallback(
+ async (category: string) => {
+ try {
+ setLoading(true);
+ setSelectedCategory(category);
+ setSelectedTag(''); // Reset tag filter when filtering by category
+
+ if (!category) {
+ fetchMarketServers();
+ return;
}
- });
-
- if (!response.ok) {
- throw new Error(`Status: ${response.status}`);
+
+ const token = localStorage.getItem('mcphub_token');
+ const response = await fetch(
+ getApiUrl(`/market/categories/${encodeURIComponent(category)}`),
+ {
+ headers: {
+ 'x-auth-token': token || '',
+ },
+ },
+ );
+
+ if (!response.ok) {
+ throw new Error(`Status: ${response.status}`);
+ }
+
+ const data: ApiResponse = await response.json();
+
+ if (data && data.success && Array.isArray(data.data)) {
+ setAllServers(data.data);
+ setCurrentPage(1);
+ applyPagination(data.data, 1);
+ } else {
+ console.error('Invalid category filter results format:', data);
+ setError(t('market.filterError'));
+ }
+ } catch (err) {
+ console.error('Error filtering servers by category:', err);
+ setError(err instanceof Error ? err.message : String(err));
+ } finally {
+ setLoading(false);
}
-
- const data: ApiResponse = await response.json();
-
- if (data && data.success && Array.isArray(data.data)) {
- setAllServers(data.data);
- setCurrentPage(1);
- applyPagination(data.data, 1);
- } else {
- console.error('Invalid category filter results format:', data);
- setError(t('market.filterError'));
- }
- } catch (err) {
- console.error('Error filtering servers by category:', err);
- setError(err instanceof Error ? err.message : String(err));
- } finally {
- setLoading(false);
- }
- }, [t, fetchMarketServers, applyPagination]);
+ },
+ [t, fetchMarketServers, applyPagination],
+ );
// Filter servers by tag
- const filterByTag = useCallback(async (tag: string) => {
- try {
- setLoading(true);
- setSelectedTag(tag);
- setSelectedCategory(''); // Reset category filter when filtering by tag
-
- if (!tag) {
- fetchMarketServers();
- return;
- }
-
- const token = localStorage.getItem('mcphub_token');
- const response = await fetch(`/api/market/tags/${encodeURIComponent(tag)}`, {
- headers: {
- 'x-auth-token': token || ''
+ const filterByTag = useCallback(
+ async (tag: string) => {
+ try {
+ setLoading(true);
+ setSelectedTag(tag);
+ setSelectedCategory(''); // Reset category filter when filtering by tag
+
+ if (!tag) {
+ fetchMarketServers();
+ return;
}
- });
-
- if (!response.ok) {
- throw new Error(`Status: ${response.status}`);
+
+ const token = localStorage.getItem('mcphub_token');
+ const response = await fetch(getApiUrl(`/market/tags/${encodeURIComponent(tag)}`), {
+ headers: {
+ 'x-auth-token': token || '',
+ },
+ });
+
+ if (!response.ok) {
+ throw new Error(`Status: ${response.status}`);
+ }
+
+ const data: ApiResponse = await response.json();
+
+ if (data && data.success && Array.isArray(data.data)) {
+ setAllServers(data.data);
+ setCurrentPage(1);
+ applyPagination(data.data, 1);
+ } else {
+ console.error('Invalid tag filter results format:', data);
+ setError(t('market.tagFilterError'));
+ }
+ } catch (err) {
+ console.error('Error filtering servers by tag:', err);
+ setError(err instanceof Error ? err.message : String(err));
+ } finally {
+ setLoading(false);
}
-
- const data: ApiResponse = await response.json();
-
- if (data && data.success && Array.isArray(data.data)) {
- setAllServers(data.data);
- setCurrentPage(1);
- applyPagination(data.data, 1);
- } else {
- console.error('Invalid tag filter results format:', data);
- setError(t('market.tagFilterError'));
- }
- } catch (err) {
- console.error('Error filtering servers by tag:', err);
- setError(err instanceof Error ? err.message : String(err));
- } finally {
- setLoading(false);
- }
- }, [t, fetchMarketServers, applyPagination]);
+ },
+ [t, fetchMarketServers, applyPagination],
+ );
// Fetch installed servers
const fetchInstalledServers = useCallback(async () => {
try {
const token = localStorage.getItem('mcphub_token');
- const response = await fetch('/api/servers', {
+ const response = await fetch(getApiUrl('/servers'), {
headers: {
- 'x-auth-token': token || ''
- }
+ 'x-auth-token': token || '',
+ },
});
-
+
if (!response.ok) {
throw new Error(`Status: ${response.status}`);
}
-
+
const data = await response.json();
-
+
if (data && data.success && Array.isArray(data.data)) {
// Extract server names
const installedServerNames = data.data.map((server: any) => server.name);
@@ -313,64 +338,77 @@ export const useMarketData = () => {
}, []);
// Check if a server is already installed
- const isServerInstalled = useCallback((serverName: string) => {
- return installedServers.includes(serverName);
- }, [installedServers]);
+ const isServerInstalled = useCallback(
+ (serverName: string) => {
+ return installedServers.includes(serverName);
+ },
+ [installedServers],
+ );
// Install server to the local environment
- const installServer = useCallback(async (server: MarketServer) => {
- try {
- const installType = server.installations?.npm ? 'npm' : Object.keys(server.installations || {}).length > 0 ? Object.keys(server.installations)[0] : null;
-
- if (!installType || !server.installations?.[installType]) {
- setError(t('market.noInstallationMethod'));
+ const installServer = useCallback(
+ async (server: MarketServer) => {
+ try {
+ const installType = server.installations?.npm
+ ? 'npm'
+ : Object.keys(server.installations || {}).length > 0
+ ? Object.keys(server.installations)[0]
+ : null;
+
+ if (!installType || !server.installations?.[installType]) {
+ setError(t('market.noInstallationMethod'));
+ return false;
+ }
+
+ const installation = server.installations[installType];
+
+ // Prepare server configuration
+ const serverConfig = {
+ name: server.name,
+ config: {
+ command: installation.command,
+ args: installation.args,
+ env: installation.env || {},
+ },
+ };
+
+ // Call the createServer API
+ const token = localStorage.getItem('mcphub_token');
+ const response = await fetch(getApiUrl('/servers'), {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'x-auth-token': token || '',
+ },
+ body: JSON.stringify(serverConfig),
+ });
+
+ if (!response.ok) {
+ const errorData = await response.json();
+ throw new Error(errorData.message || `Status: ${response.status}`);
+ }
+
+ // Update installed servers list after successful installation
+ await fetchInstalledServers();
+ return true;
+ } catch (err) {
+ console.error('Error installing server:', err);
+ setError(err instanceof Error ? err.message : String(err));
return false;
}
-
- const installation = server.installations[installType];
-
- // Prepare server configuration
- const serverConfig = {
- name: server.name,
- config: {
- command: installation.command,
- args: installation.args,
- env: installation.env || {}
- }
- };
-
- // Call the createServer API
- const token = localStorage.getItem('mcphub_token');
- const response = await fetch('/api/servers', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- 'x-auth-token': token || ''
- },
- body: JSON.stringify(serverConfig),
- });
-
- if (!response.ok) {
- const errorData = await response.json();
- throw new Error(errorData.message || `Status: ${response.status}`);
- }
-
- // Update installed servers list after successful installation
- await fetchInstalledServers();
- return true;
- } catch (err) {
- console.error('Error installing server:', err);
- setError(err instanceof Error ? err.message : String(err));
- return false;
- }
- }, [t, fetchInstalledServers]);
+ },
+ [t, fetchInstalledServers],
+ );
// Change servers per page
- const changeServersPerPage = useCallback((perPage: number) => {
- setServersPerPage(perPage);
- setCurrentPage(1);
- applyPagination(allServers, 1, perPage);
- }, [allServers, applyPagination]);
+ const changeServersPerPage = useCallback(
+ (perPage: number) => {
+ setServersPerPage(perPage);
+ setCurrentPage(1);
+ applyPagination(allServers, 1, perPage);
+ },
+ [allServers, applyPagination],
+ );
// Load initial data
useEffect(() => {
@@ -405,6 +443,6 @@ export const useMarketData = () => {
changePage,
changeServersPerPage,
// Installed servers methods
- isServerInstalled
+ isServerInstalled,
};
-};
\ No newline at end of file
+};
diff --git a/frontend/src/hooks/useServerData.ts b/frontend/src/hooks/useServerData.ts
index b88e402..69a24c3 100644
--- a/frontend/src/hooks/useServerData.ts
+++ b/frontend/src/hooks/useServerData.ts
@@ -1,18 +1,19 @@
import { useState, useEffect, useRef, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { Server, ApiResponse } from '@/types';
+import { getApiUrl } from '../utils/api';
// Configuration options
const CONFIG = {
// Initialization phase configuration
startup: {
- maxAttempts: 60, // Maximum number of attempts during initialization
- pollingInterval: 3000 // Polling interval during initialization (3 seconds)
+ maxAttempts: 60, // Maximum number of attempts during initialization
+ pollingInterval: 3000, // Polling interval during initialization (3 seconds)
},
// Normal operation phase configuration
normal: {
- pollingInterval: 10000 // Polling interval during normal operation (10 seconds)
- }
+ pollingInterval: 10000, // Polling interval during normal operation (10 seconds)
+ },
};
export const useServerData = () => {
@@ -22,7 +23,7 @@ export const useServerData = () => {
const [refreshKey, setRefreshKey] = useState(0);
const [isInitialLoading, setIsInitialLoading] = useState(true);
const [fetchAttempts, setFetchAttempts] = useState(0);
-
+
// Timer reference for polling
const intervalRef = useRef(null);
// Track current attempt count to avoid dependency cycles
@@ -40,17 +41,17 @@ export const useServerData = () => {
const startNormalPolling = useCallback(() => {
// Ensure no other timers are running
clearTimer();
-
+
const fetchServers = async () => {
try {
const token = localStorage.getItem('mcphub_token');
- const response = await fetch('/api/servers', {
+ const response = await fetch(getApiUrl('/servers'), {
headers: {
- 'x-auth-token': token || ''
- }
+ 'x-auth-token': token || '',
+ },
});
const data = await response.json();
-
+
if (data && data.success && Array.isArray(data.data)) {
setServers(data.data);
} else if (data && Array.isArray(data)) {
@@ -59,29 +60,29 @@ export const useServerData = () => {
console.error('Invalid server data format:', data);
setServers([]);
}
-
+
// Reset error state
setError(null);
} catch (err) {
console.error('Error fetching servers during normal polling:', err);
-
+
// Use friendly error message
if (!navigator.onLine) {
setError(t('errors.network'));
- } else if (err instanceof TypeError && (
- err.message.includes('NetworkError') ||
- err.message.includes('Failed to fetch')
- )) {
+ } else if (
+ err instanceof TypeError &&
+ (err.message.includes('NetworkError') || err.message.includes('Failed to fetch'))
+ ) {
setError(t('errors.serverConnection'));
} else {
setError(t('errors.serverFetch'));
}
}
};
-
+
// Execute immediately
fetchServers();
-
+
// Set up regular polling
intervalRef.current = setInterval(fetchServers, CONFIG.normal.pollingInterval);
}, [t]);
@@ -92,18 +93,18 @@ export const useServerData = () => {
attemptsRef.current = 0;
setFetchAttempts(0);
}
-
+
// Initialization phase request function
const fetchInitialData = async () => {
try {
const token = localStorage.getItem('mcphub_token');
- const response = await fetch('/api/servers', {
+ const response = await fetch(getApiUrl('/servers'), {
headers: {
- 'x-auth-token': token || ''
- }
+ 'x-auth-token': token || '',
+ },
});
const data = await response.json();
-
+
// Handle API response wrapper object, extract data field
if (data && data.success && Array.isArray(data.data)) {
setServers(data.data);
@@ -131,17 +132,17 @@ export const useServerData = () => {
// Increment attempt count, use ref to avoid triggering effect rerun
attemptsRef.current += 1;
console.error(`Initial loading attempt ${attemptsRef.current} failed:`, err);
-
+
// Update state for display
setFetchAttempts(attemptsRef.current);
-
+
// Set appropriate error message
if (!navigator.onLine) {
setError(t('errors.network'));
} else {
setError(t('errors.initialStartup'));
}
-
+
// If maximum attempt count is exceeded, give up initialization and switch to normal polling
if (attemptsRef.current >= CONFIG.startup.maxAttempts) {
console.log('Maximum startup attempts reached, switching to normal polling');
@@ -151,19 +152,19 @@ export const useServerData = () => {
// Switch to normal polling mode
startNormalPolling();
}
-
+
return false;
}
};
-
+
// On component mount, set appropriate polling based on current state
if (isInitialLoading) {
// Ensure no other timers are running
clearTimer();
-
+
// Execute initial request immediately
fetchInitialData();
-
+
// Set polling interval for initialization phase
intervalRef.current = setInterval(fetchInitialData, CONFIG.startup.pollingInterval);
console.log(`Started initial polling with interval: ${CONFIG.startup.pollingInterval}ms`);
@@ -171,7 +172,7 @@ export const useServerData = () => {
// Initialization completed, start normal polling
startNormalPolling();
}
-
+
// Cleanup function
return () => {
clearTimer();
@@ -182,21 +183,21 @@ export const useServerData = () => {
const triggerRefresh = () => {
// Clear current timer
clearTimer();
-
+
// If in initialization phase, reset initialization state
if (isInitialLoading) {
setIsInitialLoading(true);
attemptsRef.current = 0;
setFetchAttempts(0);
}
-
+
// Change in refreshKey will trigger useEffect to run again
- setRefreshKey(prevKey => prevKey + 1);
+ setRefreshKey((prevKey) => prevKey + 1);
};
// Server related operations
const handleServerAdd = () => {
- setRefreshKey(prevKey => prevKey + 1);
+ setRefreshKey((prevKey) => prevKey + 1);
};
const handleServerEdit = async (server: Server) => {
@@ -205,12 +206,12 @@ export const useServerData = () => {
const token = localStorage.getItem('mcphub_token');
const response = await fetch(`/api/settings`, {
headers: {
- 'x-auth-token': token || ''
- }
+ 'x-auth-token': token || '',
+ },
});
-
+
const settingsData: ApiResponse<{ mcpServers: Record }> = await response.json();
-
+
if (
settingsData &&
settingsData.success &&
@@ -243,8 +244,8 @@ export const useServerData = () => {
const response = await fetch(`/api/servers/${serverName}`, {
method: 'DELETE',
headers: {
- 'x-auth-token': token || ''
- }
+ 'x-auth-token': token || '',
+ },
});
const result = await response.json();
@@ -253,7 +254,7 @@ export const useServerData = () => {
return false;
}
- setRefreshKey(prevKey => prevKey + 1);
+ setRefreshKey((prevKey) => prevKey + 1);
return true;
} catch (err) {
setError(t('errors.general') + ': ' + (err instanceof Error ? err.message : String(err)));
@@ -268,7 +269,7 @@ export const useServerData = () => {
method: 'POST',
headers: {
'Content-Type': 'application/json',
- 'x-auth-token': token || ''
+ 'x-auth-token': token || '',
},
body: JSON.stringify({ enabled }),
});
@@ -282,7 +283,7 @@ export const useServerData = () => {
}
// Update the UI immediately to reflect the change
- setRefreshKey(prevKey => prevKey + 1);
+ setRefreshKey((prevKey) => prevKey + 1);
return true;
} catch (err) {
console.error('Error toggling server:', err);
@@ -301,6 +302,6 @@ export const useServerData = () => {
handleServerAdd,
handleServerEdit,
handleServerRemove,
- handleServerToggle
+ handleServerToggle,
};
-};
\ No newline at end of file
+};
diff --git a/frontend/src/hooks/useSettingsData.ts b/frontend/src/hooks/useSettingsData.ts
index 1926a74..0c20b3c 100644
--- a/frontend/src/hooks/useSettingsData.ts
+++ b/frontend/src/hooks/useSettingsData.ts
@@ -2,6 +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';
// Define types for the settings data
interface RoutingConfig {
@@ -80,7 +81,7 @@ export const useSettingsData = () => {
try {
const token = localStorage.getItem('mcphub_token');
- const response = await fetch('/api/settings', {
+ const response = await fetch(getApiUrl('/settings'), {
headers: {
'x-auth-token': token || '',
},
@@ -136,7 +137,7 @@ export const useSettingsData = () => {
try {
const token = localStorage.getItem('mcphub_token');
- const response = await fetch('/api/system-config', {
+ const response = await fetch(getApiUrl('/system-config'), {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
@@ -183,7 +184,7 @@ export const useSettingsData = () => {
try {
const token = localStorage.getItem('mcphub_token');
- const response = await fetch('/api/system-config', {
+ const response = await fetch(getApiUrl('/system-config'), {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
@@ -233,7 +234,7 @@ export const useSettingsData = () => {
try {
const token = localStorage.getItem('mcphub_token');
- const response = await fetch('/api/system-config', {
+ const response = await fetch(getApiUrl('/system-config'), {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
@@ -283,7 +284,7 @@ export const useSettingsData = () => {
try {
const token = localStorage.getItem('mcphub_token');
- const response = await fetch('/api/system-config', {
+ const response = await fetch(getApiUrl('/system-config'), {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
diff --git a/frontend/src/services/authService.ts b/frontend/src/services/authService.ts
index 0e1dd62..bf762df 100644
--- a/frontend/src/services/authService.ts
+++ b/frontend/src/services/authService.ts
@@ -1,7 +1,10 @@
-import { AuthResponse, LoginCredentials, RegisterCredentials, ChangePasswordCredentials } from '../types';
-
-// Base URL for API requests
-const API_URL = '';
+import {
+ AuthResponse,
+ LoginCredentials,
+ RegisterCredentials,
+ ChangePasswordCredentials,
+} from '../types';
+import { getApiUrl } from '../utils/api';
// Token key in localStorage
const TOKEN_KEY = 'mcphub_token';
@@ -24,7 +27,8 @@ export const removeToken = (): void => {
// Login user
export const login = async (credentials: LoginCredentials): Promise => {
try {
- const response = await fetch(`${API_URL}/auth/login`, {
+ console.log(getApiUrl('/auth/login'));
+ const response = await fetch(getApiUrl('/auth/login'), {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@@ -33,11 +37,11 @@ export const login = async (credentials: LoginCredentials): Promise => {
try {
- const response = await fetch(`${API_URL}/auth/register`, {
+ const response = await fetch(getApiUrl('/auth/register'), {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@@ -60,11 +64,11 @@ export const register = async (credentials: RegisterCredentials): Promise => {
const token = getToken();
-
+
if (!token) {
return {
success: false,
message: 'No authentication token',
};
}
-
+
try {
- const response = await fetch(`${API_URL}/auth/user`, {
+ const response = await fetch(getApiUrl('/auth/user'), {
method: 'GET',
headers: {
'x-auth-token': token,
@@ -105,18 +109,20 @@ export const getCurrentUser = async (): Promise => {
};
// Change password
-export const changePassword = async (credentials: ChangePasswordCredentials): Promise => {
+export const changePassword = async (
+ credentials: ChangePasswordCredentials,
+): Promise => {
const token = getToken();
-
+
if (!token) {
return {
success: false,
message: 'No authentication token',
};
}
-
+
try {
- const response = await fetch(`${API_URL}/auth/change-password`, {
+ const response = await fetch(getApiUrl('/auth/change-password'), {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@@ -138,4 +144,4 @@ export const changePassword = async (credentials: ChangePasswordCredentials): Pr
// Logout user
export const logout = (): void => {
removeToken();
-};
\ No newline at end of file
+};
diff --git a/frontend/src/services/logService.ts b/frontend/src/services/logService.ts
index bf60515..b4a07b6 100644
--- a/frontend/src/services/logService.ts
+++ b/frontend/src/services/logService.ts
@@ -1,5 +1,6 @@
import { useEffect, useState } from 'react';
import { getToken } from './authService'; // Import getToken function
+import { getApiUrl } from '../utils/api';
export interface LogEntry {
timestamp: number;
@@ -18,18 +19,18 @@ export const fetchLogs = async (): Promise => {
throw new Error('Authentication token not found. Please log in.');
}
- const response = await fetch('/api/logs', {
+ const response = await fetch(getApiUrl('/logs'), {
headers: {
- 'x-auth-token': token
- }
+ 'x-auth-token': token,
+ },
});
-
+
const result = await response.json();
-
+
if (!result.success) {
throw new Error(result.error || 'Failed to fetch logs');
}
-
+
return result.data;
} catch (error) {
console.error('Error fetching logs:', error);
@@ -46,15 +47,15 @@ export const clearLogs = async (): Promise => {
throw new Error('Authentication token not found. Please log in.');
}
- const response = await fetch('/api/logs', {
+ const response = await fetch(getApiUrl('/logs'), {
method: 'DELETE',
headers: {
- 'x-auth-token': token
- }
+ 'x-auth-token': token,
+ },
});
-
+
const result = await response.json();
-
+
if (!result.success) {
throw new Error(result.error || 'Failed to clear logs');
}
@@ -90,19 +91,19 @@ export const useLogs = () => {
}
// Connect to SSE endpoint with auth token in URL
- eventSource = new EventSource(`/api/logs/stream?token=${token}`);
+ eventSource = new EventSource(getApiUrl(`/logs/stream?token=${token}`));
eventSource.onmessage = (event) => {
if (!isMounted) return;
try {
const data = JSON.parse(event.data);
-
+
if (data.type === 'initial') {
setLogs(data.logs);
setLoading(false);
} else if (data.type === 'log') {
- setLogs(prevLogs => [...prevLogs, data.log]);
+ setLogs((prevLogs) => [...prevLogs, data.log]);
}
} catch (err) {
console.error('Error parsing SSE message:', err);
@@ -111,13 +112,13 @@ export const useLogs = () => {
eventSource.onerror = () => {
if (!isMounted) return;
-
+
if (eventSource) {
eventSource.close();
// Attempt to reconnect after a delay
setTimeout(connectToLogStream, 5000);
}
-
+
setError(new Error('Connection to log stream lost, attempting to reconnect...'));
};
} catch (err) {
@@ -149,4 +150,4 @@ export const useLogs = () => {
};
return { logs, loading, error, clearLogs: clearAllLogs };
-};
\ No newline at end of file
+};
diff --git a/frontend/src/utils/api.ts b/frontend/src/utils/api.ts
new file mode 100644
index 0000000..5fddcd3
--- /dev/null
+++ b/frontend/src/utils/api.ts
@@ -0,0 +1,27 @@
+/**
+ * API utility functions for constructing URLs with proper base path support
+ */
+
+/**
+ * Get the API base URL including base path and /api prefix
+ * @returns The complete API base URL
+ */
+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';
+};
+
+/**
+ * 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
+ */
+export const getApiUrl = (endpoint: string): string => {
+ const baseUrl = getApiBaseUrl();
+ // Ensure endpoint starts with /
+ const normalizedEndpoint = endpoint.startsWith('/') ? endpoint : '/' + endpoint;
+ return baseUrl + normalizedEndpoint;
+};
diff --git a/frontend/src/vite-env.d.ts b/frontend/src/vite-env.d.ts
index c66c126..e58d261 100644
--- a/frontend/src/vite-env.d.ts
+++ b/frontend/src/vite-env.d.ts
@@ -3,7 +3,7 @@
interface ImportMeta {
readonly env: {
readonly PACKAGE_VERSION: string;
- readonly VITE_BASE_PATH?: string; // Add base path environment variable
+ 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 39bd1f7..013c921 100644
--- a/frontend/vite.config.ts
+++ b/frontend/vite.config.ts
@@ -8,9 +8,12 @@ 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 || '';
+
// https://vitejs.dev/config/
export default defineConfig({
- base: './', // Use relative paths for assets
+ base: basePath || './', // Use base path or relative paths for assets
plugins: [react(), tailwindcss()],
resolve: {
alias: {
@@ -18,19 +21,20 @@ export default defineConfig({
},
},
define: {
- // Make package version available as global variable
+ // Make package version and base path available as global variables
'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
},
server: {
proxy: {
- '/api': {
+ [`${basePath}/api`]: {
target: 'http://localhost:3000',
changeOrigin: true,
},
- '/auth': {
+ [`${basePath}/auth`]: {
target: 'http://localhost:3000',
changeOrigin: true,
},
diff --git a/nginx.conf.example b/nginx.conf.example
new file mode 100644
index 0000000..780a7f8
--- /dev/null
+++ b/nginx.conf.example
@@ -0,0 +1,72 @@
+# Nginx configuration example for MCPHub with subpath routing
+# This example shows how to deploy MCPHub under a subpath like /mcphub
+
+server {
+ listen 80;
+ server_name your-domain.com;
+
+ # MCPHub under /mcphub subpath
+ location /mcphub/ {
+ # Remove the subpath prefix before forwarding to MCPHub
+ rewrite ^/mcphub/(.*)$ /$1 break;
+
+ proxy_pass http://mcphub:3000/;
+ proxy_http_version 1.1;
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection 'upgrade';
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ proxy_cache_bypass $http_upgrade;
+
+ # Important: Disable buffering for SSE connections
+ proxy_buffering off;
+ proxy_cache off;
+
+ # Support for Server-Sent Events (SSE)
+ proxy_read_timeout 24h;
+ proxy_send_timeout 24h;
+ }
+
+ # Alternative configuration if you want to keep the subpath in the backend
+ # In this case, set BASE_PATH=/mcphub
+ # location /mcphub/ {
+ # proxy_pass http://mcphub:3000/mcphub/;
+ # proxy_http_version 1.1;
+ # proxy_set_header Upgrade $http_upgrade;
+ # proxy_set_header Connection 'upgrade';
+ # proxy_set_header Host $host;
+ # proxy_set_header X-Real-IP $remote_addr;
+ # proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ # proxy_set_header X-Forwarded-Proto $scheme;
+ # proxy_cache_bypass $http_upgrade;
+ #
+ # # Important: Disable buffering for SSE connections
+ # proxy_buffering off;
+ # proxy_cache off;
+ #
+ # # Support for Server-Sent Events (SSE)
+ # proxy_read_timeout 24h;
+ # proxy_send_timeout 24h;
+ # }
+}
+
+# Docker Compose example with subpath
+# version: '3.8'
+# services:
+# mcphub:
+# image: samanhappy/mcphub
+# environment:
+# - BASE_PATH=/mcphub
+# volumes:
+# - ./mcp_settings.json:/app/mcp_settings.json
+#
+# nginx:
+# image: nginx:alpine
+# ports:
+# - "80:80"
+# volumes:
+# - ./nginx.conf:/etc/nginx/conf.d/default.conf
+# depends_on:
+# - mcphub
diff --git a/src/config/index.ts b/src/config/index.ts
index ec79f64..8bf1adc 100644
--- a/src/config/index.ts
+++ b/src/config/index.ts
@@ -10,6 +10,7 @@ const defaultConfig = {
port: process.env.PORT || 3000,
initTimeout: process.env.INIT_TIMEOUT || 300000,
timeout: process.env.REQUEST_TIMEOUT || 60000,
+ basePath: process.env.BASE_PATH || '',
mcpHubName: 'mcphub',
mcpHubVersion: getPackageVersion(),
};
diff --git a/src/middlewares/index.ts b/src/middlewares/index.ts
index a5023a3..76624ca 100644
--- a/src/middlewares/index.ts
+++ b/src/middlewares/index.ts
@@ -5,6 +5,7 @@ import { dirname } from 'path';
import fs from 'fs';
import { auth } from './auth.js';
import { initializeDefaultUser } from '../models/User.js';
+import config from '../config/index.js';
// Create __dirname equivalent for ES modules
const __filename = fileURLToPath(import.meta.url);
@@ -17,13 +18,13 @@ const findFrontendPath = (): string => {
if (fs.existsSync(devPath)) {
return path.join(dirname(__dirname), 'frontend', 'dist');
}
-
+
// Try npm/npx installed path (remove /dist directory)
const npmPath = path.join(dirname(dirname(__dirname)), 'frontend', 'dist', 'index.html');
if (fs.existsSync(npmPath)) {
return path.join(dirname(dirname(__dirname)), 'frontend', 'dist');
}
-
+
// If none of the above paths exist, return the most reasonable default path and log a warning
console.warn('Warning: Could not locate frontend files. Using default path.');
return path.join(dirname(__dirname), 'frontend', 'dist');
@@ -32,10 +33,10 @@ const findFrontendPath = (): string => {
const frontendPath = findFrontendPath();
export const errorHandler = (
- err: Error,
- _req: Request,
- res: Response,
- _next: NextFunction
+ err: Error,
+ _req: Request,
+ res: Response,
+ _next: NextFunction,
): void => {
console.error('Unhandled error:', err);
res.status(500).json({
@@ -46,10 +47,17 @@ export const errorHandler = (
export const initMiddlewares = (app: express.Application): void => {
// Serve static files from the dynamically determined frontend path
- app.use(express.static(frontendPath));
+ // Note: Static files will be handled by the server directly, not here
app.use((req, res, next) => {
- if (req.path !== '/sse' && req.path !== '/messages') {
+ const basePath = config.basePath;
+ // Only apply JSON parsing for API and auth routes, not for SSE or message endpoints
+ if (
+ req.path !== `${basePath}/sse` &&
+ req.path !== `${basePath}/messages` &&
+ !req.path.startsWith(`${basePath}/sse/`) &&
+ !req.path.startsWith(`${basePath}/mcp/`)
+ ) {
express.json()(req, res, next);
} else {
next();
@@ -57,16 +65,18 @@ export const initMiddlewares = (app: express.Application): void => {
});
// Initialize default admin user if no users exist
- initializeDefaultUser().catch(err => {
+ initializeDefaultUser().catch((err) => {
console.error('Error initializing default user:', err);
});
- // Protect all API routes with authentication middleware
- app.use('/api', auth);
-
- app.get('/', (_req: Request, res: Response) => {
- // Serve the frontend application
- res.sendFile(path.join(frontendPath, 'index.html'));
+ // Protect API routes with authentication middleware, but exclude auth endpoints
+ app.use(`${config.basePath}/api`, (req, res, next) => {
+ // Skip authentication for login and register endpoints
+ if (req.path === '/auth/login' || req.path === '/auth/register') {
+ next();
+ } else {
+ auth(req, res, next);
+ }
});
app.use(errorHandler);
diff --git a/src/routes/index.ts b/src/routes/index.ts
index 94e3a90..9e29487 100644
--- a/src/routes/index.ts
+++ b/src/routes/index.ts
@@ -1,5 +1,6 @@
import express from 'express';
import { check } from 'express-validator';
+import config from '../config/index.js';
import {
getAllServers,
getAllSettings,
@@ -7,7 +8,7 @@ import {
updateServer,
deleteServer,
toggleServer,
- updateSystemConfig
+ updateSystemConfig,
} from '../controllers/serverController.js';
import {
getGroups,
@@ -18,7 +19,7 @@ import {
addServerToExistingGroup,
removeServerFromExistingGroup,
getGroupServers,
- updateGroupServersBatch
+ updateGroupServersBatch,
} from '../controllers/groupController.js';
import {
getAllMarketServers,
@@ -27,19 +28,10 @@ import {
getAllMarketTags,
searchMarketServersByQuery,
getMarketServersByCategory,
- getMarketServersByTag
+ getMarketServersByTag,
} from '../controllers/marketController.js';
-import {
- login,
- register,
- getCurrentUser,
- changePassword
-} from '../controllers/authController.js';
-import {
- getAllLogs,
- clearLogs,
- streamLogs
-} from '../controllers/logController.js';
+import { login, register, getCurrentUser, changePassword } from '../controllers/authController.js';
+import { getAllLogs, clearLogs, streamLogs } from '../controllers/logController.js';
import { auth } from '../middlewares/auth.js';
const router = express.Router();
@@ -53,7 +45,7 @@ export const initRoutes = (app: express.Application): void => {
router.delete('/servers/:name', deleteServer);
router.post('/servers/:name/toggle', toggleServer);
router.put('/system-config', updateSystemConfig);
-
+
// Group management routes
router.get('/groups', getGroups);
router.get('/groups/:id', getGroup);
@@ -65,7 +57,7 @@ export const initRoutes = (app: express.Application): void => {
router.get('/groups/:id/servers', getGroupServers);
// New route for batch updating servers in a group
router.put('/groups/:id/servers/batch', updateGroupServersBatch);
-
+
// Market routes
router.get('/market/servers', getAllMarketServers);
router.get('/market/servers/search', searchMarketServersByQuery);
@@ -74,33 +66,45 @@ export const initRoutes = (app: express.Application): void => {
router.get('/market/categories/:category', getMarketServersByCategory);
router.get('/market/tags', getAllMarketTags);
router.get('/market/tags/:tag', getMarketServersByTag);
-
+
// Log routes
router.get('/logs', getAllLogs);
router.delete('/logs', clearLogs);
router.get('/logs/stream', streamLogs);
-
- // Auth routes (these will NOT be protected by auth middleware)
- app.post('/auth/login', [
- check('username', 'Username is required').not().isEmpty(),
- check('password', 'Password is required').not().isEmpty(),
- ], login);
-
- app.post('/auth/register', [
- check('username', 'Username is required').not().isEmpty(),
- check('password', 'Password must be at least 6 characters').isLength({ min: 6 }),
- ], register);
-
- app.get('/auth/user', auth, getCurrentUser);
-
- // Add change password route
- app.post('/auth/change-password', [
- auth,
- check('currentPassword', 'Current password is required').not().isEmpty(),
- check('newPassword', 'New password must be at least 6 characters').isLength({ min: 6 }),
- ], changePassword);
- app.use('/api', router);
+ // Auth routes - move to router instead of app directly
+ router.post(
+ '/auth/login',
+ [
+ check('username', 'Username is required').not().isEmpty(),
+ check('password', 'Password is required').not().isEmpty(),
+ ],
+ login,
+ );
+
+ router.post(
+ '/auth/register',
+ [
+ check('username', 'Username is required').not().isEmpty(),
+ check('password', 'Password must be at least 6 characters').isLength({ min: 6 }),
+ ],
+ register,
+ );
+
+ router.get('/auth/user', auth, getCurrentUser);
+
+ // Add change password route
+ router.post(
+ '/auth/change-password',
+ [
+ auth,
+ check('currentPassword', 'Current password is required').not().isEmpty(),
+ check('newPassword', 'New password must be at least 6 characters').isLength({ min: 6 }),
+ ],
+ changePassword,
+ );
+
+ app.use(`${config.basePath}/api`, router);
};
export default router;
diff --git a/src/server.ts b/src/server.ts
index c68de4b..315bdf6 100644
--- a/src/server.ts
+++ b/src/server.ts
@@ -22,10 +22,12 @@ 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 {
@@ -40,11 +42,11 @@ export class AppServer {
initUpstreamServers()
.then(() => {
console.log('MCP server initialized successfully');
- this.app.get('/sse/:group?', (req, res) => handleSseConnection(req, res));
- this.app.post('/messages', handleSseMessage);
- this.app.post('/mcp/:group?', handleMcpPostRequest);
- this.app.get('/mcp/:group?', handleMcpOtherRequest);
- this.app.delete('/mcp/:group?', handleMcpOtherRequest);
+ 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);
@@ -66,17 +68,26 @@ export class AppServer {
if (this.frontendPath) {
console.log(`Serving frontend from: ${this.frontendPath}`);
- this.app.use(express.static(this.frontendPath));
+ // Serve static files with base path
+ this.app.use(this.basePath, express.static(this.frontendPath));
- // Add the wildcard route for SPA
+ // Add the wildcard route for SPA with base path
if (fs.existsSync(path.join(this.frontendPath, 'index.html'))) {
- this.app.get('*', (_req, res) => {
+ 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.');
- this.app.get('/', (_req, res) => {
+ 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.');