feat: introduce cloud server market (#260)

This commit is contained in:
samanhappy
2025-08-09 21:14:26 +08:00
committed by GitHub
parent a9aa4a9a08
commit 26720d9e49
22 changed files with 2635 additions and 22 deletions

View File

@@ -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<void> => {
try {
const cloudServers = await getCloudServers();
const response: ApiResponse<CloudServer[]> = {
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<void> => {
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<CloudServer> = {
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<void> => {
try {
const categories = await getCloudCategories();
const response: ApiResponse<string[]> = {
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<void> => {
try {
const tags = await getCloudTags();
const response: ApiResponse<string[]> = {
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<void> => {
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<CloudServer[]> = {
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<void> => {
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<CloudServer[]> = {
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<void> => {
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<CloudServer[]> = {
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<void> => {
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<CloudTool[]> = {
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<void> => {
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,
});
}
};

View File

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

View File

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

View File

@@ -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<CloudServer[]> => {
try {
const axiosConfig = getAxiosConfig();
const mcpRouterConfig = getMCPRouterConfig();
const response = await axios.post<MCPRouterResponse<MCPRouterListServersResponse>>(
`${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<CloudServer | null> => {
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<CloudTool[]> => {
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<MCPRouterResponse<MCPRouterListToolsResponse>>(
`${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<string, any>,
): Promise<MCPRouterCallToolResponse> => {
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<MCPRouterResponse<MCPRouterCallToolResponse>>(
`${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<string[]> => {
try {
const servers = await getCloudServers();
const categories = new Set<string>();
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<string[]> => {
try {
const servers = await getCloudServers();
const tags = new Set<string>();
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<CloudServer[]> => {
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<CloudServer[]> => {
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<CloudServer[]> => {
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;
}
};

View File

@@ -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<string, any>;
}
// MCPRouter API Response types
export interface MCPRouterResponse<T = any> {
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 {