Skip to content
Merged
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
3 changes: 3 additions & 0 deletions client/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { Docs } from './pages/Docs';
import { About } from './pages/About';
import { ServerDetails } from './pages/ServerDetails';
import { Submit } from './pages/Submit';
import { Category } from './pages/Category';
import { Header } from './components/Header';
import { Footer } from './components/Footer';
import { LanguageProvider } from './contexts/LanguageContext';
Expand All @@ -21,6 +22,8 @@ function App() {
<Route path="/about" element={<About />} />
<Route path="/submit" element={<Submit />} />
<Route path="/server/:hubId" element={<ServerDetails />} />
<Route path="/category" element={<Category />} />
<Route path="/category/:categoryKey" element={<Category />} />
</Routes>
<Footer />
</div>
Expand Down
15 changes: 14 additions & 1 deletion client/src/locale/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,20 @@
},
"common": {
"yes": "Ja",
"no": "Nein"
"no": "Nein",
"loading": "Wird geladen...",
"server": "Server",
"servers": "Server",
"recommended": "Empfohlen",
"siteName": "MCP Agents Hub"
},
"category": {
"lastUpdated": "Zuletzt aktualisiert",
"noServers": "Keine Server gefunden",
"noServersDescription": "In dieser Kategorie sind derzeit keine Server verfügbar.",
"fetchError": "Fehler beim Abrufen der Kategorie-Server",
"of": "von",
"showing": "{count} von {total}"
},
"submit": {
"title": "Ihren MCP-Server einreichen",
Expand Down
15 changes: 14 additions & 1 deletion client/src/locale/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,20 @@
},
"common": {
"yes": "Yes",
"no": "No"
"no": "No",
"loading": "Loading...",
"server": "Server",
"servers": "Servers",
"recommended": "Recommended",
"siteName": "MCP Hub"
},
"category": {
"lastUpdated": "Last Updated",
"noServers": "No Servers Found",
"noServersDescription": "There are no servers available in this category at the moment.",
"fetchError": "Failed to fetch category servers",
"of": "of",
"showing": "{count} of {total}"
},
"submit": {
"title": "Submit Your MCP Server",
Expand Down
15 changes: 14 additions & 1 deletion client/src/locale/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,20 @@
},
"common": {
"yes": "Sí",
"no": "No"
"no": "No",
"loading": "Cargando...",
"server": "Servidor",
"servers": "Servidores",
"recommended": "Recomendado",
"siteName": "MCP Hub"
},
"category": {
"lastUpdated": "Última Actualización",
"noServers": "No Se Encontraron Servidores",
"noServersDescription": "No hay servidores disponibles en esta categoría en este momento.",
"fetchError": "Error al obtener servidores de categoría",
"of": "de",
"showing": "{count} de {total}"
},
"submit": {
"title": "Enviar Su Servidor MCP",
Expand Down
15 changes: 14 additions & 1 deletion client/src/locale/ja.json
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,20 @@
},
"common": {
"yes": "はい",
"no": "いいえ"
"no": "いいえ",
"loading": "読み込み中...",
"server": "サーバー",
"servers": "サーバー",
"recommended": "おすすめ",
"siteName": "MCP Hub"
},
"category": {
"lastUpdated": "最終更新",
"noServers": "サーバーが見つかりません",
"noServersDescription": "現在、このカテゴリにはサーバーがありません。",
"fetchError": "カテゴリサーバーの取得に失敗しました",
"of": "/",
"showing": "{count} / {total}"
},
"submit": {
"title": "MCPサーバーを提出",
Expand Down
15 changes: 14 additions & 1 deletion client/src/locale/zh-hans.json
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,20 @@
},
"common": {
"yes": "是",
"no": "否"
"no": "否",
"loading": "加载中...",
"server": "服务器",
"servers": "服务器",
"recommended": "推荐",
"siteName": "MCP 智能体中心"
},
"category": {
"lastUpdated": "最后更新",
"noServers": "未找到服务器",
"noServersDescription": "此类别暂时没有可用的服务器。",
"fetchError": "获取类别服务器失败",
"of": "共",
"showing": "{count} / {total}"
},
"submit": {
"title": "提交您的 MCP 服务器",
Expand Down
15 changes: 14 additions & 1 deletion client/src/locale/zh-hant.json
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,20 @@
},
"common": {
"yes": "是",
"no": "否"
"no": "否",
"loading": "載入中...",
"server": "伺服器",
"servers": "伺服器",
"recommended": "推薦",
"siteName": "MCP Agents 中心"
},
"category": {
"lastUpdated": "最後更新",
"noServers": "未找到伺服器",
"noServersDescription": "此分類目前沒有可用的伺服器。",
"fetchError": "獲取分類伺服器失敗",
"of": "共",
"showing": "{count} / {total}"
},
"submit": {
"title": "提交您的 MCP 伺服器",
Expand Down
140 changes: 140 additions & 0 deletions client/src/pages/Category.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import { useState, useEffect, useMemo } from 'react';
import { useParams } from 'react-router-dom';
import { useLanguage } from '../contexts/LanguageContext';
import { ServerCard } from '../components/ServerCard';
import { MCPServer } from '../types';
import { ChevronRight } from 'lucide-react';

export function Category() {
const { t, language } = useLanguage();
const { categoryKey } = useParams();
const [servers, setServers] = useState<MCPServer[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [visibleCount, setVisibleCount] = useState(12);
const LOAD_MORE_STEP = 12;

useEffect(() => {
const fetchCategoryServers = async () => {
if (!categoryKey) return;

try {
setIsLoading(true);
const url = `/v1/hub/search_servers?categoryKey=${categoryKey}&locale=${language || 'en'}`;
const response = await fetch(url);

if (!response.ok) {
throw new Error(t('category.fetchError') || 'Failed to fetch category servers');
}

const data = await response.json();
setServers(data);
} catch (error) {
console.error('Error fetching category servers:', error);
setServers([]);
} finally {
setIsLoading(false);
}
};

fetchCategoryServers();
// Reset visible count when category changes
setVisibleCount(12);
}, [categoryKey, language]);

const visibleServers = useMemo(() => {
return servers.slice(0, visibleCount);
}, [servers, visibleCount]);

const handleLoadMore = () => {
setVisibleCount(prev => Math.min(prev + LOAD_MORE_STEP, servers.length));
};

const formatCategoryName = (key: string) => {
return key
.split('-')
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ');
};

return (
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
{/* Breadcrumbs */}
{categoryKey && (
<nav className="mb-4">
<ol className="flex items-center space-x-2 text-sm text-gray-600">
<li>
<a href="/" className="hover:text-indigo-600 transition-colors duration-200">
{t('home.title') || 'MCP Hub'}
</a>
</li>
<li className="flex items-center">
<svg className="h-4 w-4 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
</li>
<li className="font-medium text-gray-900">
{t(`category.${categoryKey}`) || t(`category-common.${categoryKey}`) || formatCategoryName(categoryKey)}
</li>
</ol>
</nav>
)}

{/* Toolbar with statistics */}
{!isLoading && servers.length > 0 && (
<div className="bg-white rounded-lg shadow-sm border border-gray-100 p-4 flex items-center justify-between mb-6">
<div className="flex items-center space-x-4">
<div className="px-3 py-1 bg-blue-50 rounded-md text-blue-700 font-medium text-sm">
{servers.length} {servers.length === 1 ? t('common.server') : t('common.servers')}
</div>
<div className="text-sm text-gray-600">
<span className="font-medium">{servers.filter(s => s.isRecommended).length}</span> {t('common.recommended')}
</div>
</div>
<div className="text-sm text-gray-500">
{t('details.lastUpdated') || t('category.lastUpdated') || "Last Updated"}: {new Date().toLocaleDateString(language === 'en' ? 'en-US' : language === 'ja' ? 'ja-JP' : language === 'es' ? 'es-ES' : language === 'de' ? 'de-DE' : language === 'zh-hans' ? 'zh-CN' : language === 'zh-hant' ? 'zh-TW' : 'en-US')}
</div>
</div>
)}

{isLoading ? (
<div className="py-12 text-center">
<div className="inline-block h-8 w-8 animate-spin rounded-full border-4 border-solid border-indigo-600 border-r-transparent"></div>
<p className="mt-4 text-gray-600">{t('common.loading')}</p>
</div>
) : servers.length === 0 ? (
<div className="bg-white rounded-xl shadow-md p-8 text-center">
<h2 className="text-xl font-semibold text-gray-800 mb-2">
{t('category.noServers')}
</h2>
<p className="text-gray-600">
{t('category.noServersDescription')}
</p>
</div>
) : (
<div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{visibleServers.map((server) => (
<ServerCard key={server.mcpId} server={server} />
))}
</div>

{visibleCount < servers.length && (
<div className="flex justify-end mt-6">
<button
onClick={handleLoadMore}
className="text-indigo-600 hover:text-indigo-800 transition-colors duration-300
flex items-center text-sm font-medium group"
>
<span>{t('home.loadMore')}</span>
<span className="text-xs mx-2">
({visibleCount} {t('home.of')} {servers.length})
</span>
<ChevronRight className="h-5 w-5 ml-1 group-hover:translate-x-1 transition-transform" strokeWidth={2.5} />
</button>
</div>
)}
</div>
)}
</div>
);
}
2 changes: 1 addition & 1 deletion client/src/pages/ServerDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@ export function ServerDetails() {
<div className="flex flex-wrap items-center justify-start gap-4">
<div className="flex items-center">
<span className="text-sm font-medium text-gray-500 mr-2">{t('details.category')}:</span>
<span className="text-gray-900">{server.category || t('details.notSpecified')}</span>
<span className="text-gray-900">{server.category ? t(`category.${server.category}`) : t('details.notSpecified')}</span>
</div>
<div className="flex items-center">
<span className="text-sm font-medium text-gray-500 mr-2">{t('details.requiresApiKey')}:</span>
Expand Down
37 changes: 37 additions & 0 deletions server/src/routes/hub.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,43 @@ router.get('/servers', async (req: Request, res: Response): Promise<void> => {
}
});

// GET /search_servers - returns server data filtered by category and locale
router.get('/search_servers', async (req: Request, res: Response): Promise<void> => {
try {
console.log('api /search_servers called: req.query = ', req.query);
// Get parameters from query
const categoryKey = req.query.categoryKey as string;
const requestedLocale = (req.query.locale as string) || 'en';

// Get servers from cache for the requested locale
const mcpServersCache = await refreshCacheIfNeeded(requestedLocale);

// Filter by category if categoryKey is provided
let filteredServers = mcpServersCache;
if (categoryKey) {
filteredServers = mcpServersCache.filter(server => server.category === categoryKey);
console.log(`Filtered servers by category: ${categoryKey}, found ${filteredServers.length} servers`);
}

// Sort servers so that isRecommended: true servers appear first
filteredServers.sort((a, b) => {
// If a is recommended and b is not, a comes first
if (a.isRecommended && !b.isRecommended) return -1;
// If b is recommended and a is not, b comes first
if (!a.isRecommended && b.isRecommended) return 1;
// If both have the same recommendation status, maintain original order
return 0;
});

// Return filtered and sorted data
res.json(filteredServers);
console.log(`v1/hub/search_servers Served filtered and sorted MCP servers data (recommended first) for locale: ${requestedLocale}${categoryKey ? `, category: ${categoryKey}` : ''} at ${new Date().toISOString()}`);
} catch (error) {
console.error('Error serving filtered MCP servers:', error);
res.status(500).json({ error: 'Internal server error' });
}
});

// GET /servers/:hubId - returns a specific server by hubId with enriched data from GitHub README
router.get('/servers/:hubId', async (req: Request, res: Response): Promise<void> => {
try {
Expand Down