toggleSection('password')}
diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts
index bcfa03b..1b725ae 100644
--- a/frontend/src/types/index.ts
+++ b/frontend/src/types/index.ts
@@ -55,6 +55,27 @@ export interface MarketServer {
is_official?: boolean;
}
+// Cloud Server types (for MCPRouter API)
+export interface CloudServer {
+ created_at: string;
+ updated_at: string;
+ name: string;
+ author_name: string;
+ title: string;
+ description: string;
+ content: string;
+ server_key: string;
+ config_name: string;
+ server_url: string;
+ tools?: CloudServerTool[];
+}
+
+export interface CloudServerTool {
+ name: string;
+ description: string;
+ inputSchema: Record;
+}
+
// Tool input schema types
export interface ToolInputSchema {
type: string;
diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js
index 9b004be..231f303 100644
--- a/frontend/tailwind.config.js
+++ b/frontend/tailwind.config.js
@@ -1,12 +1,9 @@
/** @type {import('tailwindcss').Config} */
export default {
- content: [
- "./index.html",
- "./src/**/*.{js,ts,jsx,tsx}",
- ],
+ content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
darkMode: 'class', // Use class strategy for dark mode
theme: {
extend: {},
},
- plugins: [],
-}
\ No newline at end of file
+ plugins: [require('@tailwindcss/line-clamp')],
+};
diff --git a/locales/en.json b/locales/en.json
index 0659e32..5b7baa4 100644
--- a/locales/en.json
+++ b/locales/en.json
@@ -196,7 +196,8 @@
"users": "Users",
"settings": "Settings",
"changePassword": "Change Password",
- "market": "Market",
+ "market": "Local Market",
+ "cloud": "Cloud Market",
"logs": "Logs"
},
"pages": {
@@ -281,7 +282,7 @@
"configureTools": "Configure Tools"
},
"market": {
- "title": "Server Market",
+ "title": "Local Market",
"official": "Official",
"by": "By",
"unknown": "Unknown",
@@ -324,6 +325,58 @@
"confirmVariablesMessage": "Please ensure these variables are properly defined in your runtime environment. Continue installing server?",
"confirmAndInstall": "Confirm and Install"
},
+ "cloud": {
+ "title": "Cloud Market",
+ "subtitle": "Powered by MCPRouter",
+ "by": "By",
+ "server": "Server",
+ "config": "Config",
+ "created": "Created",
+ "updated": "Updated",
+ "available": "Available",
+ "description": "Description",
+ "details": "Details",
+ "tools": "Tools",
+ "tool": "tool",
+ "toolsAvailable": "{{count}} tool available||{{count}} tools available",
+ "loadingTools": "Loading tools...",
+ "noTools": "No tools available for this server",
+ "noDescription": "No description available",
+ "viewDetails": "View Details",
+ "parameters": "Parameters",
+ "result": "Result",
+ "error": "Error",
+ "callTool": "Call",
+ "calling": "Calling...",
+ "toolCallSuccess": "Tool {{toolName}} executed successfully",
+ "toolCallError": "Failed to call tool {{toolName}}: {{error}}",
+ "viewSchema": "View Schema",
+ "backToList": "Back to Cloud Market",
+ "search": "Search",
+ "searchPlaceholder": "Search cloud servers by name, title, or author",
+ "clearFilters": "Clear Filters",
+ "clearCategoryFilter": "Clear",
+ "clearTagFilter": "Clear",
+ "categories": "Categories",
+ "tags": "Tags",
+ "noCategories": "No categories found",
+ "noTags": "No tags found",
+ "noServers": "No cloud servers found",
+ "fetchError": "Error fetching cloud servers",
+ "serverNotFound": "Cloud server not found",
+ "searchError": "Error searching cloud servers",
+ "filterError": "Error filtering cloud servers by category",
+ "tagFilterError": "Error filtering cloud servers by tag",
+ "showing": "Showing {{from}}-{{to}} of {{total}} cloud servers",
+ "perPage": "Per page",
+ "apiKeyNotConfigured": "MCPRouter API key not configured",
+ "apiKeyNotConfiguredDescription": "To use cloud servers, you need to configure your MCPRouter API key.",
+ "getApiKey": "Get API Key",
+ "configureInSettings": "Configure in Settings",
+ "installServer": "Install {{name}}",
+ "installSuccess": "Server {{name}} installed successfully",
+ "installError": "Failed to install server: {{error}}"
+ },
"tool": {
"run": "Run",
"running": "Running...",
@@ -394,7 +447,20 @@
"openaiApiEmbeddingModelPlaceholder": "text-embedding-3-small",
"smartRoutingConfigUpdated": "Smart routing configuration updated successfully",
"smartRoutingRequiredFields": "Database URL and OpenAI API Key are required to enable smart routing",
- "smartRoutingValidationError": "Please fill in the required fields before enabling Smart Routing: {{fields}}"
+ "smartRoutingValidationError": "Please fill in the required fields before enabling Smart Routing: {{fields}}",
+ "mcpRouterConfig": "Cloud Market",
+ "mcpRouterApiKey": "MCPRouter API Key",
+ "mcpRouterApiKeyDescription": "API key for accessing MCPRouter cloud market services",
+ "mcpRouterApiKeyPlaceholder": "Enter MCPRouter API key",
+ "mcpRouterReferer": "Referer",
+ "mcpRouterRefererDescription": "Referer header for MCPRouter API requests",
+ "mcpRouterRefererPlaceholder": "https://mcphub.app",
+ "mcpRouterTitle": "Title",
+ "mcpRouterTitleDescription": "Title header for MCPRouter API requests",
+ "mcpRouterTitlePlaceholder": "MCPHub",
+ "mcpRouterBaseUrl": "Base URL",
+ "mcpRouterBaseUrlDescription": "Base URL for MCPRouter API",
+ "mcpRouterBaseUrlPlaceholder": "https://api.mcprouter.to/v1"
},
"dxt": {
"upload": "Upload",
diff --git a/locales/zh.json b/locales/zh.json
index 4753add..8407932 100644
--- a/locales/zh.json
+++ b/locales/zh.json
@@ -197,7 +197,8 @@
"changePassword": "修改密码",
"groups": "分组",
"users": "用户",
- "market": "市场",
+ "market": "本地市场",
+ "cloud": "云端市场",
"logs": "日志"
},
"pages": {
@@ -229,7 +230,7 @@
"title": "用户管理"
},
"market": {
- "title": "服务器市场 - (数据来源于 mcpm.sh)"
+ "title": "本地市场 - (数据来源于 mcpm.sh)"
},
"logs": {
"title": "系统日志"
@@ -282,7 +283,7 @@
"configureTools": "配置工具"
},
"market": {
- "title": "服务器市场",
+ "title": "本地市场",
"official": "官方",
"by": "作者",
"unknown": "未知",
@@ -314,7 +315,7 @@
"required": "必填",
"example": "示例",
"viewSchema": "查看结构",
- "fetchError": "获取服务器市场数据失败",
+ "fetchError": "获取本地市场服务器数据失败",
"serverNotFound": "未找到服务器",
"searchError": "搜索服务器失败",
"filterError": "按分类筛选服务器失败",
@@ -325,6 +326,58 @@
"confirmVariablesMessage": "请确保这些变量在运行环境中已正确定义。是否继续安装服务器?",
"confirmAndInstall": "确认并安装"
},
+ "cloud": {
+ "title": "云端市场",
+ "subtitle": "由 MCPRouter 提供支持",
+ "by": "作者",
+ "server": "服务器",
+ "config": "配置",
+ "created": "创建时间",
+ "updated": "更新时间",
+ "available": "可用",
+ "description": "描述",
+ "details": "详细信息",
+ "tools": "工具",
+ "tool": "个工具",
+ "toolsAvailable": "{{count}} 个工具可用",
+ "loadingTools": "加载工具中...",
+ "noTools": "该服务器没有可用工具",
+ "noDescription": "无描述信息",
+ "viewDetails": "查看详情",
+ "parameters": "参数",
+ "result": "结果",
+ "error": "错误",
+ "callTool": "调用",
+ "calling": "调用中...",
+ "toolCallSuccess": "工具 {{toolName}} 执行成功",
+ "toolCallError": "调用工具 {{toolName}} 失败:{{error}}",
+ "viewSchema": "查看结构",
+ "backToList": "返回云端市场",
+ "search": "搜索",
+ "searchPlaceholder": "搜索云端服务器名称、标题或作者",
+ "clearFilters": "清除筛选",
+ "clearCategoryFilter": "清除",
+ "clearTagFilter": "清除",
+ "categories": "分类",
+ "tags": "标签",
+ "noCategories": "未找到分类",
+ "noTags": "未找到标签",
+ "noServers": "未找到云端服务器",
+ "fetchError": "获取云端服务器失败",
+ "serverNotFound": "未找到云端服务器",
+ "searchError": "搜索云端服务器失败",
+ "filterError": "按分类筛选云端服务器失败",
+ "tagFilterError": "按标签筛选云端服务器失败",
+ "showing": "显示 {{from}}-{{to}}/{{total}} 个云端服务器",
+ "perPage": "每页显示",
+ "apiKeyNotConfigured": "MCPRouter API 密钥未配置",
+ "apiKeyNotConfiguredDescription": "要使用云端服务器,您需要配置 MCPRouter API 密钥。",
+ "getApiKey": "获取 API 密钥",
+ "configureInSettings": "在设置中配置",
+ "installServer": "安装 {{name}}",
+ "installSuccess": "服务器 {{name}} 安装成功",
+ "installError": "安装服务器失败:{{error}}"
+ },
"tool": {
"run": "运行",
"running": "运行中...",
@@ -396,7 +449,20 @@
"openaiApiEmbeddingModelPlaceholder": "text-embedding-3-small",
"smartRoutingConfigUpdated": "智能路由配置更新成功",
"smartRoutingRequiredFields": "启用智能路由需要填写数据库连接地址和 OpenAI API 密钥",
- "smartRoutingValidationError": "启用智能路由前请先填写必要字段:{{fields}}"
+ "smartRoutingValidationError": "启用智能路由前请先填写必要字段:{{fields}}",
+ "mcpRouterConfig": "云端市场",
+ "mcpRouterApiKey": "MCPRouter API 密钥",
+ "mcpRouterApiKeyDescription": "用于访问 MCPRouter 云端市场服务的 API 密钥",
+ "mcpRouterApiKeyPlaceholder": "请输入 MCPRouter API 密钥",
+ "mcpRouterReferer": "引用地址",
+ "mcpRouterRefererDescription": "MCPRouter API 请求的引用地址头",
+ "mcpRouterRefererPlaceholder": "https://mcphub.app",
+ "mcpRouterTitle": "标题",
+ "mcpRouterTitleDescription": "MCPRouter API 请求的标题头",
+ "mcpRouterTitlePlaceholder": "MCPHub",
+ "mcpRouterBaseUrl": "基础地址",
+ "mcpRouterBaseUrlDescription": "MCPRouter API 的基础地址",
+ "mcpRouterBaseUrlPlaceholder": "https://api.mcprouter.to/v1"
},
"dxt": {
"upload": "上传",
diff --git a/package.json b/package.json
index 3d781d9..424b080 100644
--- a/package.json
+++ b/package.json
@@ -75,6 +75,7 @@
"@shadcn/ui": "^0.0.4",
"@swc/core": "^1.13.0",
"@swc/jest": "^0.2.39",
+ "@tailwindcss/line-clamp": "^0.4.4",
"@tailwindcss/postcss": "^4.1.3",
"@tailwindcss/vite": "^4.1.7",
"@types/bcryptjs": "^3.0.0",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index befe6a1..962789c 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -93,6 +93,9 @@ importers:
'@swc/jest':
specifier: ^0.2.39
version: 0.2.39(@swc/core@1.13.0)
+ '@tailwindcss/line-clamp':
+ specifier: ^0.4.4
+ version: 0.4.4(tailwindcss@4.1.11)
'@tailwindcss/postcss':
specifier: ^4.1.3
version: 4.1.11
@@ -1445,6 +1448,11 @@ packages:
'@swc/types@0.1.23':
resolution: {integrity: sha512-u1iIVZV9Q0jxY+yM2vw/hZGDNudsN85bBpTqzAQ9rzkxW9D+e3aEM4Han+ow518gSewkXgjmEK0BD79ZcNVgPw==}
+ '@tailwindcss/line-clamp@0.4.4':
+ resolution: {integrity: sha512-5U6SY5z8N42VtrCrKlsTAA35gy2VSyYtHWCsg1H87NU1SXnEfekTVlrga9fzUDrrHcGi2Lb5KenUWb4lRQT5/g==}
+ peerDependencies:
+ tailwindcss: '>=2.0.0 || >=3.0.0 || >=3.0.0-alpha.1'
+
'@tailwindcss/node@4.1.11':
resolution: {integrity: sha512-yzhzuGRmv5QyU9qLNg4GTlYI6STedBWRE7NjxP45CsFYYq9taI0zJXZBMqIC/c8fViNLhmrbpSFS57EoxUmD6Q==}
@@ -5486,6 +5494,10 @@ snapshots:
dependencies:
'@swc/counter': 0.1.3
+ '@tailwindcss/line-clamp@0.4.4(tailwindcss@4.1.11)':
+ dependencies:
+ tailwindcss: 4.1.11
+
'@tailwindcss/node@4.1.11':
dependencies:
'@ampproject/remapping': 2.3.0
diff --git a/src/controllers/cloudController.ts b/src/controllers/cloudController.ts
new file mode 100644
index 0000000..2e6b8da
--- /dev/null
+++ b/src/controllers/cloudController.ts
@@ -0,0 +1,273 @@
+import { Request, Response } from 'express';
+import { ApiResponse, CloudServer, CloudTool } from '../types/index.js';
+import {
+ getCloudServers,
+ getCloudServerByName,
+ getCloudServerTools,
+ callCloudServerTool,
+ getCloudCategories,
+ getCloudTags,
+ searchCloudServers,
+ filterCloudServersByCategory,
+ filterCloudServersByTag,
+} from '../services/cloudService.js';
+
+// Get all cloud market servers
+export const getAllCloudServers = async (_: Request, res: Response): Promise => {
+ try {
+ const cloudServers = await getCloudServers();
+ const response: ApiResponse = {
+ success: true,
+ data: cloudServers,
+ };
+ res.json(response);
+ } catch (error) {
+ console.error('Error getting cloud market servers:', error);
+ const errorMessage =
+ error instanceof Error ? error.message : 'Failed to get cloud market servers';
+ res.status(500).json({
+ success: false,
+ message: errorMessage,
+ });
+ }
+};
+
+// Get a specific cloud market server by name
+export const getCloudServer = async (req: Request, res: Response): Promise => {
+ try {
+ const { name } = req.params;
+ if (!name) {
+ res.status(400).json({
+ success: false,
+ message: 'Server name is required',
+ });
+ return;
+ }
+
+ const server = await getCloudServerByName(name);
+ if (!server) {
+ res.status(404).json({
+ success: false,
+ message: 'Cloud server not found',
+ });
+ return;
+ }
+
+ // Fetch tools for this server
+ try {
+ const tools = await getCloudServerTools(server.server_key);
+ server.tools = tools;
+ } catch (toolError) {
+ console.warn(`Failed to fetch tools for server ${server.name}:`, toolError);
+ // Continue without tools
+ }
+
+ const response: ApiResponse = {
+ success: true,
+ data: server,
+ };
+ res.json(response);
+ } catch (error) {
+ console.error('Error getting cloud market server:', error);
+ const errorMessage =
+ error instanceof Error ? error.message : 'Failed to get cloud market server';
+ res.status(500).json({
+ success: false,
+ message: errorMessage,
+ });
+ }
+};
+
+// Get all cloud market categories
+export const getAllCloudCategories = async (_: Request, res: Response): Promise => {
+ try {
+ const categories = await getCloudCategories();
+ const response: ApiResponse = {
+ success: true,
+ data: categories,
+ };
+ res.json(response);
+ } catch (error) {
+ console.error('Error getting cloud market categories:', error);
+ const errorMessage =
+ error instanceof Error ? error.message : 'Failed to get cloud market categories';
+ res.status(500).json({
+ success: false,
+ message: errorMessage,
+ });
+ }
+};
+
+// Get all cloud market tags
+export const getAllCloudTags = async (_: Request, res: Response): Promise => {
+ try {
+ const tags = await getCloudTags();
+ const response: ApiResponse = {
+ success: true,
+ data: tags,
+ };
+ res.json(response);
+ } catch (error) {
+ console.error('Error getting cloud market tags:', error);
+ const errorMessage = error instanceof Error ? error.message : 'Failed to get cloud market tags';
+ res.status(500).json({
+ success: false,
+ message: errorMessage,
+ });
+ }
+};
+
+// Search cloud market servers
+export const searchCloudServersByQuery = async (req: Request, res: Response): Promise => {
+ try {
+ const query = req.query.query as string;
+ if (!query || query.trim() === '') {
+ res.status(400).json({
+ success: false,
+ message: 'Search query is required',
+ });
+ return;
+ }
+
+ const servers = await searchCloudServers(query);
+ const response: ApiResponse = {
+ success: true,
+ data: servers,
+ };
+ res.json(response);
+ } catch (error) {
+ console.error('Error searching cloud market servers:', error);
+ const errorMessage =
+ error instanceof Error ? error.message : 'Failed to search cloud market servers';
+ res.status(500).json({
+ success: false,
+ message: errorMessage,
+ });
+ }
+};
+
+// Get cloud market servers by category
+export const getCloudServersByCategory = async (req: Request, res: Response): Promise => {
+ try {
+ const { category } = req.params;
+ if (!category) {
+ res.status(400).json({
+ success: false,
+ message: 'Category is required',
+ });
+ return;
+ }
+
+ const servers = await filterCloudServersByCategory(category);
+ const response: ApiResponse = {
+ success: true,
+ data: servers,
+ };
+ res.json(response);
+ } catch (error) {
+ console.error('Error getting cloud market servers by category:', error);
+ const errorMessage =
+ error instanceof Error ? error.message : 'Failed to get cloud market servers by category';
+ res.status(500).json({
+ success: false,
+ message: errorMessage,
+ });
+ }
+};
+
+// Get cloud market servers by tag
+export const getCloudServersByTag = async (req: Request, res: Response): Promise => {
+ try {
+ const { tag } = req.params;
+ if (!tag) {
+ res.status(400).json({
+ success: false,
+ message: 'Tag is required',
+ });
+ return;
+ }
+
+ const servers = await filterCloudServersByTag(tag);
+ const response: ApiResponse = {
+ success: true,
+ data: servers,
+ };
+ res.json(response);
+ } catch (error) {
+ console.error('Error getting cloud market servers by tag:', error);
+ const errorMessage =
+ error instanceof Error ? error.message : 'Failed to get cloud market servers by tag';
+ res.status(500).json({
+ success: false,
+ message: errorMessage,
+ });
+ }
+};
+
+// Get tools for a specific cloud server
+export const getCloudServerToolsList = async (req: Request, res: Response): Promise => {
+ try {
+ const { serverName } = req.params;
+ if (!serverName) {
+ res.status(400).json({
+ success: false,
+ message: 'Server name is required',
+ });
+ return;
+ }
+
+ const tools = await getCloudServerTools(serverName);
+ const response: ApiResponse = {
+ success: true,
+ data: tools,
+ };
+ res.json(response);
+ } catch (error) {
+ console.error('Error getting cloud server tools:', error);
+ const errorMessage =
+ error instanceof Error ? error.message : 'Failed to get cloud server tools';
+ res.status(500).json({
+ success: false,
+ message: errorMessage,
+ });
+ }
+};
+
+// Call a tool on a cloud server
+export const callCloudTool = async (req: Request, res: Response): Promise => {
+ try {
+ const { serverName, toolName } = req.params;
+ const { arguments: args } = req.body;
+
+ if (!serverName) {
+ res.status(400).json({
+ success: false,
+ message: 'Server name is required',
+ });
+ return;
+ }
+
+ if (!toolName) {
+ res.status(400).json({
+ success: false,
+ message: 'Tool name is required',
+ });
+ return;
+ }
+
+ const result = await callCloudServerTool(serverName, toolName, args || {});
+ const response: ApiResponse = {
+ success: true,
+ data: result,
+ };
+ res.json(response);
+ } catch (error) {
+ console.error('Error calling cloud server tool:', error);
+ const errorMessage =
+ error instanceof Error ? error.message : 'Failed to call cloud server tool';
+ res.status(500).json({
+ success: false,
+ message: errorMessage,
+ });
+ }
+};
diff --git a/src/controllers/serverController.ts b/src/controllers/serverController.ts
index 7743ef0..9e17f5e 100644
--- a/src/controllers/serverController.ts
+++ b/src/controllers/serverController.ts
@@ -505,7 +505,7 @@ export const updateToolDescription = async (req: Request, res: Response): Promis
export const updateSystemConfig = (req: Request, res: Response): void => {
try {
- const { routing, install, smartRouting } = req.body;
+ const { routing, install, smartRouting, mcpRouter } = req.body;
const currentUser = (req as any).user;
if (
@@ -524,7 +524,12 @@ export const updateSystemConfig = (req: Request, res: Response): void => {
typeof smartRouting.dbUrl !== 'string' &&
typeof smartRouting.openaiApiBaseUrl !== 'string' &&
typeof smartRouting.openaiApiKey !== 'string' &&
- typeof smartRouting.openaiApiEmbeddingModel !== 'string'))
+ typeof smartRouting.openaiApiEmbeddingModel !== 'string')) &&
+ (!mcpRouter ||
+ (typeof mcpRouter.apiKey !== 'string' &&
+ typeof mcpRouter.referer !== 'string' &&
+ typeof mcpRouter.title !== 'string' &&
+ typeof mcpRouter.baseUrl !== 'string'))
) {
res.status(400).json({
success: false,
@@ -555,6 +560,12 @@ export const updateSystemConfig = (req: Request, res: Response): void => {
openaiApiKey: '',
openaiApiEmbeddingModel: '',
},
+ mcpRouter: {
+ apiKey: '',
+ referer: 'https://mcphub.app',
+ title: 'MCPHub',
+ baseUrl: 'https://api.mcprouter.to/v1',
+ },
};
}
@@ -586,6 +597,15 @@ export const updateSystemConfig = (req: Request, res: Response): void => {
};
}
+ if (!settings.systemConfig.mcpRouter) {
+ settings.systemConfig.mcpRouter = {
+ apiKey: '',
+ referer: 'https://mcphub.app',
+ title: 'MCPHub',
+ baseUrl: 'https://api.mcprouter.to/v1',
+ };
+ }
+
if (routing) {
if (typeof routing.enableGlobalRoute === 'boolean') {
settings.systemConfig.routing.enableGlobalRoute = routing.enableGlobalRoute;
@@ -676,6 +696,21 @@ export const updateSystemConfig = (req: Request, res: Response): void => {
needsSync = (!wasSmartRoutingEnabled && isNowEnabled) || (isNowEnabled && hasConfigChanged);
}
+ if (mcpRouter) {
+ if (typeof mcpRouter.apiKey === 'string') {
+ settings.systemConfig.mcpRouter.apiKey = mcpRouter.apiKey;
+ }
+ if (typeof mcpRouter.referer === 'string') {
+ settings.systemConfig.mcpRouter.referer = mcpRouter.referer;
+ }
+ if (typeof mcpRouter.title === 'string') {
+ settings.systemConfig.mcpRouter.title = mcpRouter.title;
+ }
+ if (typeof mcpRouter.baseUrl === 'string') {
+ settings.systemConfig.mcpRouter.baseUrl = mcpRouter.baseUrl;
+ }
+ }
+
if (saveSettings(settings, currentUser)) {
res.json({
success: true,
diff --git a/src/routes/index.ts b/src/routes/index.ts
index f4789d1..da34ed4 100644
--- a/src/routes/index.ts
+++ b/src/routes/index.ts
@@ -43,6 +43,17 @@ import {
getMarketServersByCategory,
getMarketServersByTag,
} from '../controllers/marketController.js';
+import {
+ getAllCloudServers,
+ getCloudServer,
+ getAllCloudCategories,
+ getAllCloudTags,
+ searchCloudServersByQuery,
+ getCloudServersByCategory,
+ getCloudServersByTag,
+ getCloudServerToolsList,
+ callCloudTool,
+} from '../controllers/cloudController.js';
import { login, register, getCurrentUser, changePassword } from '../controllers/authController.js';
import { getAllLogs, clearLogs, streamLogs } from '../controllers/logController.js';
import { getRuntimeConfig, getPublicConfig } from '../controllers/configController.js';
@@ -103,6 +114,17 @@ export const initRoutes = (app: express.Application): void => {
router.get('/market/tags', getAllMarketTags);
router.get('/market/tags/:tag', getMarketServersByTag);
+ // Cloud Market routes
+ router.get('/cloud/servers', getAllCloudServers);
+ router.get('/cloud/servers/search', searchCloudServersByQuery);
+ router.get('/cloud/servers/:name', getCloudServer);
+ router.get('/cloud/categories', getAllCloudCategories);
+ router.get('/cloud/categories/:category', getCloudServersByCategory);
+ router.get('/cloud/tags', getAllCloudTags);
+ router.get('/cloud/tags/:tag', getCloudServersByTag);
+ router.get('/cloud/servers/:serverName/tools', getCloudServerToolsList);
+ router.post('/cloud/servers/:serverName/tools/:toolName/call', callCloudTool);
+
// Log routes
router.get('/logs', getAllLogs);
router.delete('/logs', clearLogs);
diff --git a/src/services/cloudService.ts b/src/services/cloudService.ts
new file mode 100644
index 0000000..188e1cc
--- /dev/null
+++ b/src/services/cloudService.ts
@@ -0,0 +1,273 @@
+import axios, { AxiosRequestConfig } from 'axios';
+import {
+ CloudServer,
+ CloudTool,
+ MCPRouterResponse,
+ MCPRouterListServersResponse,
+ MCPRouterListToolsResponse,
+ MCPRouterCallToolResponse,
+} from '../types/index.js';
+import { loadOriginalSettings } from '../config/index.js';
+
+// MCPRouter API default base URL
+const DEFAULT_MCPROUTER_API_BASE = 'https://api.mcprouter.to/v1';
+
+// Get MCPRouter API config from system configuration
+const getMCPRouterConfig = () => {
+ const settings = loadOriginalSettings();
+ const mcpRouterConfig = settings.systemConfig?.mcpRouter;
+
+ return {
+ apiKey: mcpRouterConfig?.apiKey || process.env.MCPROUTER_API_KEY || '',
+ referer: mcpRouterConfig?.referer || process.env.MCPROUTER_REFERER || 'https://mcphub.app',
+ title: mcpRouterConfig?.title || process.env.MCPROUTER_TITLE || 'MCPHub',
+ baseUrl:
+ mcpRouterConfig?.baseUrl || process.env.MCPROUTER_API_BASE || DEFAULT_MCPROUTER_API_BASE,
+ };
+};
+
+// Get axios config with MCPRouter headers
+const getAxiosConfig = (): AxiosRequestConfig => {
+ const mcpRouterConfig = getMCPRouterConfig();
+
+ return {
+ headers: {
+ Authorization: mcpRouterConfig.apiKey ? `Bearer ${mcpRouterConfig.apiKey}` : '',
+ 'HTTP-Referer': mcpRouterConfig.referer || 'https://mcphub.app',
+ 'X-Title': mcpRouterConfig.title || 'MCPHub',
+ 'Content-Type': 'application/json',
+ },
+ };
+};
+
+// List all available cloud servers
+export const getCloudServers = async (): Promise => {
+ try {
+ const axiosConfig = getAxiosConfig();
+ const mcpRouterConfig = getMCPRouterConfig();
+
+ const response = await axios.post>(
+ `${mcpRouterConfig.baseUrl}/list-servers`,
+ {},
+ axiosConfig,
+ );
+
+ const data = response.data;
+
+ if (data.code !== 0) {
+ throw new Error(data.message || 'Failed to fetch servers');
+ }
+
+ return data.data.servers || [];
+ } catch (error) {
+ console.error('Error fetching cloud market servers:', error);
+ throw error;
+ }
+};
+
+// Get a specific cloud server by name
+export const getCloudServerByName = async (name: string): Promise => {
+ try {
+ const servers = await getCloudServers();
+ return servers.find((server) => server.name === name || server.config_name === name) || null;
+ } catch (error) {
+ console.error(`Error fetching cloud server ${name}:`, error);
+ throw error;
+ }
+};
+
+// List tools for a specific cloud server
+export const getCloudServerTools = async (serverKey: string): Promise => {
+ try {
+ const axiosConfig = getAxiosConfig();
+ const mcpRouterConfig = getMCPRouterConfig();
+
+ if (
+ !axiosConfig.headers?.['Authorization'] ||
+ axiosConfig.headers['Authorization'] === 'Bearer '
+ ) {
+ throw new Error('MCPROUTER_API_KEY_NOT_CONFIGURED');
+ }
+
+ const response = await axios.post>(
+ `${mcpRouterConfig.baseUrl}/list-tools`,
+ {
+ server: serverKey,
+ },
+ axiosConfig,
+ );
+
+ const data = response.data;
+ if (data.code !== 0) {
+ throw new Error(data.message || 'Failed to fetch tools');
+ }
+
+ return data.data.tools || [];
+ } catch (error) {
+ console.error(`Error fetching tools for server ${serverKey}:`, error);
+ throw error;
+ }
+};
+
+// Call a tool on a cloud server
+export const callCloudServerTool = async (
+ serverName: string,
+ toolName: string,
+ args: Record,
+): Promise => {
+ try {
+ const axiosConfig = getAxiosConfig();
+ const mcpRouterConfig = getMCPRouterConfig();
+
+ if (
+ !axiosConfig.headers?.['Authorization'] ||
+ axiosConfig.headers['Authorization'] === 'Bearer '
+ ) {
+ throw new Error('MCPROUTER_API_KEY_NOT_CONFIGURED');
+ }
+
+ const response = await axios.post>(
+ `${mcpRouterConfig.baseUrl}/call-tool`,
+ {
+ server: serverName,
+ name: toolName,
+ arguments: args,
+ },
+ axiosConfig,
+ );
+
+ const data = response.data;
+
+ if (data.code !== 0) {
+ throw new Error(data.message || 'Failed to call tool');
+ }
+
+ return data.data;
+ } catch (error) {
+ console.error(`Error calling tool ${toolName} on server ${serverName}:`, error);
+ throw error;
+ }
+};
+
+// Get all categories from cloud servers
+export const getCloudCategories = async (): Promise => {
+ try {
+ const servers = await getCloudServers();
+ const categories = new Set();
+
+ servers.forEach((server) => {
+ // Extract categories from content or description
+ // This is a simple implementation, you might want to parse the content more sophisticatedly
+ if (server.content) {
+ const categoryMatches = server.content.match(/category[:\s]*([^,\n]+)/gi);
+ if (categoryMatches) {
+ categoryMatches.forEach((match) => {
+ const category = match.replace(/category[:\s]*/i, '').trim();
+ if (category) categories.add(category);
+ });
+ }
+ }
+ });
+
+ return Array.from(categories).sort();
+ } catch (error) {
+ console.error('Error fetching cloud market categories:', error);
+ throw error;
+ }
+};
+
+// Get all tags from cloud servers
+export const getCloudTags = async (): Promise => {
+ try {
+ const servers = await getCloudServers();
+ const tags = new Set();
+
+ servers.forEach((server) => {
+ // Extract tags from content or description
+ if (server.content) {
+ const tagMatches = server.content.match(/tag[s]?[:\s]*([^,\n]+)/gi);
+ if (tagMatches) {
+ tagMatches.forEach((match) => {
+ const tag = match.replace(/tag[s]?[:\s]*/i, '').trim();
+ if (tag) tags.add(tag);
+ });
+ }
+ }
+ });
+
+ return Array.from(tags).sort();
+ } catch (error) {
+ console.error('Error fetching cloud market tags:', error);
+ throw error;
+ }
+};
+
+// Search cloud servers by query
+export const searchCloudServers = async (query: string): Promise => {
+ try {
+ const servers = await getCloudServers();
+ const searchTerms = query
+ .toLowerCase()
+ .split(' ')
+ .filter((term) => term.length > 0);
+
+ if (searchTerms.length === 0) {
+ return servers;
+ }
+
+ return servers.filter((server) => {
+ const searchText = [
+ server.name,
+ server.title,
+ server.description,
+ server.content,
+ server.author_name,
+ ]
+ .join(' ')
+ .toLowerCase();
+
+ return searchTerms.some((term) => searchText.includes(term));
+ });
+ } catch (error) {
+ console.error('Error searching cloud market servers:', error);
+ throw error;
+ }
+};
+
+// Filter cloud servers by category
+export const filterCloudServersByCategory = async (category: string): Promise => {
+ try {
+ const servers = await getCloudServers();
+
+ if (!category) {
+ return servers;
+ }
+
+ return servers.filter((server) => {
+ const content = (server.content || '').toLowerCase();
+ return content.includes(category.toLowerCase());
+ });
+ } catch (error) {
+ console.error('Error filtering cloud market servers by category:', error);
+ throw error;
+ }
+};
+
+// Filter cloud servers by tag
+export const filterCloudServersByTag = async (tag: string): Promise => {
+ try {
+ const servers = await getCloudServers();
+
+ if (!tag) {
+ return servers;
+ }
+
+ return servers.filter((server) => {
+ const content = (server.content || '').toLowerCase();
+ return content.includes(tag.toLowerCase());
+ });
+ } catch (error) {
+ console.error('Error filtering cloud market servers by tag:', error);
+ throw error;
+ }
+};
diff --git a/src/types/index.ts b/src/types/index.ts
index 55f2fb9..f3bcb44 100644
--- a/src/types/index.ts
+++ b/src/types/index.ts
@@ -81,6 +81,49 @@ export interface MarketServer {
is_official?: boolean;
}
+// Cloud Market Server types (for MCPRouter API)
+export interface CloudServer {
+ created_at: string;
+ updated_at: string;
+ name: string;
+ author_name: string;
+ title: string;
+ description: string;
+ content: string;
+ server_key: string;
+ config_name: string;
+ tools?: CloudTool[];
+}
+
+export interface CloudTool {
+ name: string;
+ description: string;
+ inputSchema: Record;
+}
+
+// MCPRouter API Response types
+export interface MCPRouterResponse {
+ code: number;
+ message: string;
+ data: T;
+}
+
+export interface MCPRouterListServersResponse {
+ servers: CloudServer[];
+}
+
+export interface MCPRouterListToolsResponse {
+ tools: CloudTool[];
+}
+
+export interface MCPRouterCallToolResponse {
+ content: Array<{
+ type: string;
+ text: string;
+ }>;
+ isError: boolean;
+}
+
export interface SystemConfig {
routing?: {
enableGlobalRoute?: boolean; // Controls whether the /sse endpoint without group is enabled
@@ -95,6 +138,12 @@ export interface SystemConfig {
baseUrl?: string; // Base URL for group card copy operations
};
smartRouting?: SmartRoutingConfig;
+ mcpRouter?: {
+ apiKey?: string; // MCPRouter API key for authentication
+ referer?: string; // Referer header for MCPRouter API requests
+ title?: string; // Title header for MCPRouter API requests
+ baseUrl?: string; // Base URL for MCPRouter API (default: https://api.mcprouter.to/v1)
+ };
}
export interface UserConfig {