Skip to main content
Glama

MiniMax MCP JS

Official
by MiniMax-AI
mcp-sse-server.ts42.1 kB
import express, { Request, Response, NextFunction } from 'express'; import cors from 'cors'; import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js'; import { Config, MusicGenerationRequest, VoiceDesignRequest } from './types/index.js'; import { ConfigManager } from './config/ConfigManager.js'; import { DEFAULT_SERVER_ENDPOINT, DEFAULT_SERVER_PORT, ERROR_API_KEY_REQUIRED, ERROR_API_HOST_REQUIRED, RESOURCE_MODE_URL, TRANSPORT_MODE_SSE, DEFAULT_VOICE_ID, DEFAULT_SPEECH_MODEL, DEFAULT_SPEED, DEFAULT_VOLUME, DEFAULT_PITCH, DEFAULT_EMOTION, DEFAULT_FORMAT, DEFAULT_SAMPLE_RATE, DEFAULT_BITRATE, DEFAULT_CHANNEL, DEFAULT_LANGUAGE_BOOST, DEFAULT_T2I_MODEL, DEFAULT_VIDEO_MODEL } from './const/index.js'; import { MiniMaxAPI } from './utils/api.js'; import { TTSAPI } from './api/tts.js'; import { ImageAPI } from './api/image.js'; import { VideoAPI } from './api/video.js'; import { VoiceCloneAPI } from './api/voice-clone.js'; import { VoiceAPI } from './api/voice.js'; import { MusicAPI } from './api/music.js'; import { VoiceDesignAPI } from './api/voice-design.js'; import { playAudio } from './utils/audio.js'; import { z } from 'zod'; import 'dotenv/config'; import { COMMON_PARAMETERS_SCHEMA } from './schema/index.js'; // Heartbeat interval in milliseconds const HEARTBEAT_INTERVAL = 30000; // Retry configuration const MAX_RETRY_ATTEMPTS = 3; const RETRY_DELAY = 1000; /** * Connection information interface for managing SSE connections */ interface ConnectionInfo { transport: SSEServerTransport; heartbeatInterval: NodeJS.Timeout | null; lastActivityTime: number; } /** * MCP SSE Server class, handles SSE transport mode * Supports multiple client connections and health checks */ export class MCPSSEServer { private server: any; private mcpServer: McpServer; private connections: Map<string, ConnectionInfo> = new Map(); private config!: Config; private api: MiniMaxAPI; private ttsApi: TTSAPI; private imageApi: ImageAPI; private videoApi: VideoAPI; private voiceCloneApi: VoiceCloneAPI; private voiceApi: VoiceAPI; private musicApi: MusicAPI; private voiceDesignApi: VoiceDesignAPI; private connectionMonitorInterval: NodeJS.Timeout | null = null; /** * Create an SSE server instance * @param customConfig Configuration object */ constructor(customConfig?: Partial<Config>) { // Initialize configuration using ConfigManager this.initializeConfig(customConfig); // Create API instances this.api = new MiniMaxAPI(this.config); this.ttsApi = new TTSAPI(this.api); this.imageApi = new ImageAPI(this.api); this.videoApi = new VideoAPI(this.api); this.voiceCloneApi = new VoiceCloneAPI(this.api); this.voiceApi = new VoiceAPI(this.api); this.musicApi = new MusicAPI(this.api); this.voiceDesignApi = new VoiceDesignAPI(this.api); // Create MCP server instance this.mcpServer = new McpServer({ name: 'minimax-mcp-js', version: '1.0.0', }); // Register tools this.registerTools(); } /** * Initialize configuration * @param customConfig Custom configuration */ private initializeConfig(customConfig?: Partial<Config>): void { // Use ConfigManager to get configuration, automatically handling priorities this.config = ConfigManager.getConfig(customConfig); // Ensure SSE transport mode is used if (this.config.server) { this.config.server.mode = TRANSPORT_MODE_SSE; } else { this.config.server = { mode: TRANSPORT_MODE_SSE, port: DEFAULT_SERVER_PORT, endpoint: DEFAULT_SERVER_ENDPOINT }; } // console.log(`[${new Date().toISOString()}] SSE server configuration initialized`); } /** * Update configuration and recreate API instances * @param newConfig New configuration object */ public updateConfig(newConfig: Partial<Config>): void { // Use ConfigManager to merge configurations this.config = ConfigManager.getConfig(newConfig, this.config); // Ensure SSE transport mode is used if (this.config.server) { this.config.server.mode = TRANSPORT_MODE_SSE; } // Update API instances this.api = new MiniMaxAPI(this.config); this.ttsApi = new TTSAPI(this.api); this.imageApi = new ImageAPI(this.api); this.videoApi = new VideoAPI(this.api); this.voiceCloneApi = new VoiceCloneAPI(this.api); this.voiceApi = new VoiceAPI(this.api); this.musicApi = new MusicAPI(this.api); this.voiceDesignApi = new VoiceDesignAPI(this.api); // console.log(`[${new Date().toISOString()}] SSE server configuration updated`); } /** * Register all MCP tools */ private registerTools(): void { this.registerTextToAudioTool(); this.registerListVoicesTool(); this.registerPlayAudioTool(); this.registerVoiceCloneTool(); this.registerTextToImageTool(); this.registerGenerateVideoTool(); this.registerImageToVideoTool(); this.registerQueryVideoGenerationTool(); this.registerMusicGenerationTool(); this.registerVoiceDesignTool(); } /** * Register text-to-speech tool */ private registerTextToAudioTool(): void { this.mcpServer.tool( 'text_to_audio', 'Convert text to audio with a given voice and save the output audio file to a given directory. If no directory is provided, the file will be saved to desktop. If no voice ID is provided, the default voice will be used.\n\nNote: This tool calls MiniMax API and may incur costs. Use only when explicitly requested by the user.', { text: z.string().describe('Text to convert to audio'), outputDirectory: COMMON_PARAMETERS_SCHEMA.outputDirectory, voiceId: z.string().optional().default(DEFAULT_VOICE_ID).describe('Voice ID to use, e.g. "female-shaonv"'), model: z.string().optional().default(DEFAULT_SPEECH_MODEL).describe('Model to use'), speed: z.number().min(0.5).max(2.0).optional().default(DEFAULT_SPEED).describe('Speech speed'), vol: z.number().min(0.1).max(10.0).optional().default(DEFAULT_VOLUME).describe('Speech volume'), pitch: z.number().min(-12).max(12).optional().default(DEFAULT_PITCH).describe('Speech pitch'), emotion: z .string() .optional() .default(DEFAULT_EMOTION) .describe('Speech emotion, values: ["happy", "sad", "angry", "fearful", "disgusted", "surprised", "neutral"]'), format: z .string() .optional() .default(DEFAULT_FORMAT) .describe('Audio format, values: ["pcm", "mp3","flac", "wav"]'), sampleRate: z .number() .optional() .default(DEFAULT_SAMPLE_RATE) .describe('Sample rate (Hz), values: [8000, 16000, 22050, 24000, 32000, 44100]'), bitrate: z .number() .optional() .default(DEFAULT_BITRATE) .describe('Bitrate (bps), values: [64000, 96000, 128000, 160000, 192000, 224000, 256000, 320000]'), channel: z.number().optional().default(DEFAULT_CHANNEL).describe('Audio channels, values: [1, 2]'), languageBoost: z.string().optional().default(DEFAULT_LANGUAGE_BOOST).describe(`Enhance the ability to recognize specified languages and dialects. Supported values include: 'Chinese', 'Chinese,Yue', 'English', 'Arabic', 'Russian', 'Spanish', 'French', 'Portuguese', 'German', 'Turkish', 'Dutch', 'Ukrainian', 'Vietnamese', 'Indonesian', 'Japanese', 'Italian', 'Korean', 'Thai', 'Polish', 'Romanian', 'Greek', 'Czech', 'Finnish', 'Hindi', 'auto', default is 'auto'`), subtitleEnable: z.boolean().optional().default(false).describe(`The parameter controls whether the subtitle service is enabled. The model must be 'speech-01-turbo' or 'speech-01-hd'. If this parameter is not provided, the default value is false`), outputFile: z .string() .optional() .describe('Path to save the generated audio file, automatically generated if not provided'), }, async (args, extra) => { try { // Build TTS request parameters const ttsParams = { text: args.text, outputDirectory: args.outputDirectory, voiceId: args.voiceId || DEFAULT_VOICE_ID, model: args.model || DEFAULT_SPEECH_MODEL, speed: args.speed || DEFAULT_SPEED, vol: args.vol || DEFAULT_VOLUME, pitch: args.pitch || DEFAULT_PITCH, emotion: args.emotion || DEFAULT_EMOTION, format: args.format || DEFAULT_FORMAT, sampleRate: args.sampleRate || DEFAULT_SAMPLE_RATE, bitrate: args.bitrate || DEFAULT_BITRATE, channel: args.channel || DEFAULT_CHANNEL, languageBoost: args.languageBoost || DEFAULT_LANGUAGE_BOOST, subtitleEnable: args.subtitleEnable || false, outputFile: args.outputFile, }; // Use global configuration const requestApiKey = this.config.apiKey; if (!requestApiKey) { throw new Error(ERROR_API_KEY_REQUIRED); } // Update configuration with request-specific parameters const requestConfig: Partial<Config> = { apiKey: requestApiKey, apiHost: this.config.apiHost, resourceMode: this.config.resourceMode, }; // Update API instance const requestApi = new MiniMaxAPI(requestConfig as Config); const requestTtsApi = new TTSAPI(requestApi); // Automatically set resource mode (if not specified) const outputFormat = requestConfig.resourceMode; const ttsRequest = { ...ttsParams, outputFormat, }; // If no output filename is provided, generate one automatically if (!ttsRequest.outputFile) { const textPrefix = ttsRequest.text.substring(0, 20).replace(/[^\w]/g, '_'); ttsRequest.outputFile = `tts_${textPrefix}_${Date.now()}`; } const result = await requestTtsApi.generateSpeech(ttsRequest); // Return different messages based on output format if (outputFormat === RESOURCE_MODE_URL) { return { content: [ { type: 'text', text: `Success. Audio URL: ${result.audio}. ${ttsParams.subtitleEnable ? `Subtitle file saved: ${result.subtitle}` : ''}`, }, ], }; } else { return { content: [ { type: 'text', text: `Audio file saved: ${result.audio}. ${ttsParams.subtitleEnable ? `Subtitle file saved: ${result.subtitle}. ` : ''}Voice used: ${ttsParams.voiceId}`, }, ], }; } } catch (error) { return { content: [ { type: 'text', text: `Failed to generate audio: ${error instanceof Error ? error.message : String(error)}`, }, ], }; } }, ); } /** * Register list voices tool */ private registerListVoicesTool(): void { this.mcpServer.tool( 'list_voices', 'List all available voices. Only supported when api_host is https://api.minimax.chat.', { voiceType: z .string() .optional() .default('all') .describe('Type of voices to list, values: ["all", "system", "voice_cloning"]'), }, async (params) => { try { const result = await this.voiceApi.listVoices(params); return { content: [ { type: 'text', text: `Success. System voices: ${result.systemVoices.join(', ')}, Cloned voices: ${result.voiceCloneVoices.join(', ')}`, }, ], }; } catch (error) { return { content: [ { type: 'text', text: `Failed to list voices: ${error instanceof Error ? error.message : String(error)}`, }, ], }; } }, ); } /** * Register play audio tool */ private registerPlayAudioTool(): void { this.mcpServer.tool( 'play_audio', 'Play an audio file. Supports WAV and MP3 formats. Does not support video.', { inputFilePath: z.string().describe('Path to the audio file to play'), isUrl: z.boolean().optional().default(false).describe('Whether the audio file is a URL'), }, async (params) => { try { const result = await playAudio(params); return { content: [ { type: 'text', text: result, }, ], }; } catch (error) { return { content: [ { type: 'text', text: `Failed to play audio: ${error instanceof Error ? error.message : String(error)}`, }, ], }; } }, ); } /** * Register voice clone tool */ private registerVoiceCloneTool(): void { this.mcpServer.tool( 'voice_clone', 'Clone a voice using the provided audio file. New voices will incur costs when first used.\n\nNote: This tool calls MiniMax API and may incur costs. Use only when explicitly requested by the user.', { voiceId: z.string().describe('Voice ID to use'), audioFile: z.string().describe('Path to the audio file'), text: z.string().optional().describe('Text for the demo audio'), outputDirectory: COMMON_PARAMETERS_SCHEMA.outputDirectory, isUrl: z.boolean().optional().default(false).describe('Whether the audio file is a URL'), }, async (params) => { try { const result = await this.voiceCloneApi.cloneVoice(params); return { content: [ { type: 'text', text: `Voice cloning successful: ${result}`, }, ], }; } catch (error) { // Check if this is a real-name verification error const errorMessage = error instanceof Error ? error.message : String(error); if (errorMessage.includes('voice clone user forbidden') || errorMessage.includes('should complete real-name verification')) { // Domestic platform verification URL const verificationUrl = 'https://platform.minimaxi.com/user-center/basic-information'; return { content: [ { type: 'text', text: `Voice cloning failed: Real-name verification required. To use voice cloning feature, please:\n\n1. Visit MiniMax platform (${verificationUrl})\n2. Complete the real-name verification process\n3. Try again after verification is complete\n\nThis requirement is for security and compliance purposes.`, }, ], }; } // Regular error handling return { content: [ { type: 'text', text: `Voice cloning failed: ${error instanceof Error ? error.message : String(error)}`, }, ], }; } }, ); } /** * Register text-to-image tool */ private registerTextToImageTool(): void { this.mcpServer.tool( 'text_to_image', 'Generate images based on text prompts.\n\nNote: This tool calls MiniMax API and may incur costs. Use only when explicitly requested by the user.', { model: z.string().optional().default(DEFAULT_T2I_MODEL).describe('Model to use'), prompt: z.string().describe('Text prompt for image generation'), aspectRatio: z .string() .optional() .default('1:1') .describe('Image aspect ratio, values: ["1:1", "16:9","4:3", "3:2", "2:3", "3:4", "9:16", "21:9"]'), n: z.number().min(1).max(9).optional().default(1).describe('Number of images to generate'), promptOptimizer: z.boolean().optional().default(true).describe('Whether to optimize the prompt'), outputDirectory: COMMON_PARAMETERS_SCHEMA.outputDirectory, outputFile: z .string() .optional() .describe('Path to save the generated image file, automatically generated if not provided'), }, async (params) => { try { // If no output filename is provided, generate one automatically if (!params.outputFile) { const promptPrefix = params.prompt.substring(0, 20).replace(/[^\w]/g, '_'); params.outputFile = `image_${promptPrefix}_${Date.now()}`; } const outputFiles = await this.imageApi.generateImage(params); // Handle different output formats if (this.config.resourceMode === RESOURCE_MODE_URL) { return { content: [ { type: 'text', text: `Success. Image URL(s): ${outputFiles.join(', ')}`, }, ], }; } else { return { content: [ { type: 'text', text: `Image(s) saved: ${outputFiles.join(', ')}`, }, ], }; } } catch (error) { return { content: [ { type: 'text', text: `Failed to generate image: ${error instanceof Error ? error.message : String(error)}`, }, ], }; } }, ); } /** * Register generate video tool */ private registerGenerateVideoTool(): void { this.mcpServer.tool( 'generate_video', 'Generate a video based on text prompts.\n\nNote: This tool calls MiniMax API and may incur costs. Use only when explicitly requested by the user.', { model: z .string() .optional() .default(DEFAULT_VIDEO_MODEL) .describe('Model to use, values: ["T2V-01", "T2V-01-Director", "I2V-01", "I2V-01-Director", "I2V-01-live", "MiniMax-Hailuo-02"]'), prompt: z.string().describe('Text prompt for video generation'), firstFrameImage: z.string().optional().describe('First frame image'), duration: z.number().optional().describe('The duration of the video. The model must be "MiniMax-Hailuo-02". Values can be 6 and 10.'), resolution: z.string().optional().describe('The resolution of the video. The model must be "MiniMax-Hailuo-02". Values range ["768P", "1080P"]'), outputDirectory: COMMON_PARAMETERS_SCHEMA.outputDirectory, outputFile: z .string() .optional() .describe('Path to save the generated video file, automatically generated if not provided'), asyncMode: z .boolean() .optional() .default(false) .describe('Whether to use async mode. Defaults to False. If True, the video generation task will be submitted asynchronously and the response will return a task_id. Should use `query_video_generation` tool to check the status of the task and get the result.'), }, async (params) => { try { // If no output filename is provided, generate one automatically if (!params.outputFile) { const promptPrefix = params.prompt.substring(0, 20).replace(/[^\w]/g, '_'); params.outputFile = `video_${promptPrefix}_${Date.now()}`; } const result = await this.videoApi.generateVideo(params); if (params.asyncMode) { return { content: [ { type: 'text', text: `Success. Video generation task submitted: Task ID: ${result.task_id}. Please use \`query_video_generation\` tool to check the status of the task and get the result.`, }, ], }; } // Handle different output formats if (this.config.resourceMode === RESOURCE_MODE_URL) { return { content: [ { type: 'text', text: `Success. Video URL: ${result.video_url}`, }, ], }; } else { return { content: [ { type: 'text', text: `Video saved: ${result.video_path}`, }, ], }; } } catch (error) { return { content: [ { type: 'text', text: `Failed to generate video: ${error instanceof Error ? error.message : String(error)}`, }, ], }; } }, ); } /** * Register image-to-video tool */ private registerImageToVideoTool(): void { this.mcpServer.tool( 'image_to_video', 'Generate a video based on an image.\n\nNote: This tool calls MiniMax API and may incur costs. Use only when explicitly requested by the user.', { model: z .string() .optional() .default('I2V-01') .describe('Model to use, values: ["I2V-01", "I2V-01-Director", "I2V-01-live"]'), prompt: z.string().describe('Text prompt for video generation'), firstFrameImage: z.string().describe('Path to the first frame image'), outputDirectory: COMMON_PARAMETERS_SCHEMA.outputDirectory, outputFile: z .string() .optional() .describe('Path to save the generated video file, automatically generated if not provided'), asyncMode: z .boolean() .optional() .default(false) .describe('Whether to use async mode. Defaults to False. If True, the video generation task will be submitted asynchronously and the response will return a task_id. Should use `query_video_generation` tool to check the status of the task and get the result.'), }, async (params) => { try { // If no output filename is provided, generate one automatically if (!params.outputFile) { const promptPrefix = params.prompt.substring(0, 20).replace(/[^\w]/g, '_'); params.outputFile = `i2v_${promptPrefix}_${Date.now()}`; } const result = await this.videoApi.generateVideo(params); if (params.asyncMode) { return { content: [ { type: 'text', text: `Success. Video generation task submitted: Task ID: ${result.task_id}. Please use \`query_video_generation\` tool to check the status of the task and get the result.`, }, ], }; } // Handle different output formats if (this.config.resourceMode === RESOURCE_MODE_URL) { return { content: [ { type: 'text', text: `Success. Video URL: ${result.video_url}`, }, ], }; } else { return { content: [ { type: 'text', text: `Video saved: ${result.video_path}`, }, ], }; } } catch (error) { return { content: [ { type: 'text', text: `Failed to generate video: ${error instanceof Error ? error.message : String(error)}`, }, ], }; } } ); } /** * Register query video generation tool */ private registerQueryVideoGenerationTool(): void { this.mcpServer.tool( 'query_video_generation', 'Query the status of a video generation task.', { taskId: z .string() .describe('The Task ID to query. Should be the task_id returned by `generate_video` tool if `async_mode` is True.'), outputDirectory: COMMON_PARAMETERS_SCHEMA.outputDirectory, }, async (params: any) => { try { // No need to update configuration from request parameters in stdio mode const result = await this.videoApi.queryVideoGeneration(params); if (result.status === 'Success') { if (this.config.resourceMode === RESOURCE_MODE_URL) { return { content: [ { type: 'text', text: `Success. Video URL: ${result.video_url}`, }, ], }; } else { return { content: [ { type: 'text', text: `Success. Video saved as: ${result.video_path}`, }, ], }; } } else { return { content: [ { type: 'text', text: `Video generation task is still processing: Task ID: ${params.taskId}.`, }, ], }; } } catch (error) { return { content: [ { type: 'text', text: `Failed to query video generation: ${error instanceof Error ? error.message : String(error)}`, }, ], }; } }, ); } /** * Register music generation tool */ private registerMusicGenerationTool(): void { this.mcpServer.tool( 'music_generation', 'Create a music generation task using AI models. Generate music from prompt and lyrics.\n\nNote: This tool calls MiniMax API and may incur costs. Use only when explicitly requested by the user.', { prompt: z .string() .describe('Music creation inspiration describing style, mood, scene, etc.\nExample: "Pop music, sad, suitable for rainy nights". Character range: [10, 300]'), lyrics: z .string() .describe('Song lyrics for music generation.\nUse newline (\\n) to separate each line of lyrics. Supports lyric structure tags [Intro][Verse][Chorus][Bridge][Outro]\nto enhance musicality. Character range: [10, 600] (each Chinese character, punctuation, and letter counts as 1 character)'), sampleRate: z .number() .optional() .default(DEFAULT_SAMPLE_RATE) .describe('Sample rate of generated music. Values: [16000, 24000, 32000, 44100]'), bitrate: z .number() .optional() .default(DEFAULT_BITRATE) .describe('Bitrate of generated music. Values: [32000, 64000, 128000, 256000]'), format: z .string() .optional() .default(DEFAULT_FORMAT) .describe('Format of generated music. Values: ["mp3", "wav", "pcm"]'), outputDirectory: COMMON_PARAMETERS_SCHEMA.outputDirectory, }, async (params: MusicGenerationRequest) => { try { // Automatically set resource mode (if not specified) const outputFormat = this.config.resourceMode; const musicRequest = { ...params, outputFormat, }; // No need to update configuration from request parameters in stdio mode const outputFile = await this.musicApi.generateMusic(musicRequest); // Handle different output formats if (this.config.resourceMode === RESOURCE_MODE_URL) { return { content: [ { type: 'text', text: `Success. Music URL(s): ${outputFile}`, }, ], }; } else { return { content: [ { type: 'text', text: `Success. Music saved as: ${outputFile}`, }, ], }; } } catch (error) { return { content: [ { type: 'text', text: `Failed to generate music: ${error instanceof Error ? error.message : String(error)}`, }, ], }; } }, ); } /** * Register voice design tool */ private registerVoiceDesignTool(): void { this.mcpServer.tool( 'voice_design', 'Generate a voice based on description prompts.\n\nNote: This tool calls MiniMax API and may incur costs. Use only when explicitly requested by the user.', { prompt: z .string() .describe('The prompt to generate the voice from'), previewText: z .string() .describe('The text to preview the voice'), voiceId: z .string() .optional() .describe('The id of the voice to use. For example, "male-qn-qingse"/"audiobook_female_1"/"cute_boy"/"Charming_Lady"...'), outputDirectory: COMMON_PARAMETERS_SCHEMA.outputDirectory, }, async (params: VoiceDesignRequest) => { try { // No need to update configuration from request parameters in stdio mode const { voiceId, outputFile } = await this.voiceDesignApi.voiceDesign(params); // Handle different output formats if (this.config.resourceMode === RESOURCE_MODE_URL) { return { content: [ { type: 'text', text: `Success. Voice ID: ${voiceId}. Voice URL: ${outputFile}`, }, ], }; } else { return { content: [ { type: 'text', text: `Success. Voice ID: ${voiceId}. Voice saved as: ${outputFile}`, }, ], }; } } catch (error) { return { content: [ { type: 'text', text: `Failed to design voice: ${error instanceof Error ? error.message : String(error)}`, }, ], }; } }, ); } /** * Start SSE server * @returns Promise<void> */ public async start(): Promise<void> { try { // Validate necessary configuration if (!this.config.apiKey) { throw new Error(ERROR_API_KEY_REQUIRED); } if (!this.config.apiHost) { throw new Error(ERROR_API_HOST_REQUIRED); } const port = this.config.server?.port || DEFAULT_SERVER_PORT; const endpoint = this.config.server?.endpoint || DEFAULT_SERVER_ENDPOINT; // Create Express application const app = express(); // Configure CORS - more flexible configuration app.use(cors({ origin: process.env.ALLOWED_ORIGINS?.split(',') || '*', methods: ['GET', 'POST'], allowedHeaders: ['Content-Type', 'Authorization'] })); app.use(express.json()); // Configure SSE route app.get('/sse', async (req, res) => { try { // Create SSE transport instance const transport = new SSEServerTransport(endpoint, res); const sessionId = transport.sessionId || `session-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`; // console.log(`[${new Date().toISOString()}] New SSE connection established: ${sessionId}`); // Set response headers to prevent connection timeout res.setHeader('Content-Type', 'text/event-stream'); res.setHeader('Cache-Control', 'no-cache'); res.setHeader('Connection', 'keep-alive'); // Start heartbeat mechanism const heartbeatInterval = setInterval(() => { try { res.write(`event: heartbeat\ndata: ${Date.now()}\n\n`); // console.log(`[${new Date().toISOString()}] Heartbeat sent: ${sessionId}`); } catch (error) { // console.error(`[${new Date().toISOString()}] Failed to send heartbeat: ${sessionId}`, error); this.closeConnection(sessionId); } }, HEARTBEAT_INTERVAL); // Save connection information this.connections.set(sessionId, { transport, heartbeatInterval, lastActivityTime: Date.now() }); // Handle connection close req.on('close', () => { // console.log(`[${new Date().toISOString()}] SSE connection closed: ${sessionId}`); this.closeConnection(sessionId); }); // Connect to MCP server await this.mcpServer.connect(transport); // console.log(`[${new Date().toISOString()}] MCP server connection successful: ${sessionId}`); // Send initial connection confirmation event res.write(`event: connected\ndata: {"sessionId":"${sessionId}","timestamp":${Date.now()}}\n\n`); } catch (error) { // console.error(`[${new Date().toISOString()}] Failed to establish SSE connection:`, error); res.status(500).end(); } }); // Configure message handling route app.post(endpoint, async (req, res) => { try { // console.log(`[${new Date().toISOString()}] Received client message:`, req.query); const sessionId = req.query.sessionId as string; if (sessionId && this.connections.has(sessionId)) { // If sessionId is provided and exists, use the specified connection const connectionInfo = this.connections.get(sessionId); if (connectionInfo) { // Update last activity time connectionInfo.lastActivityTime = Date.now(); // Handle message await this.handleClientMessage(connectionInfo.transport, req, res); return; } } // If no sessionId is specified or connection not found, but there are active connections if (this.connections.size > 0) { // Use the first available connection (simple implementation) const [firstSessionId, connectionInfo] = [...this.connections.entries()][0]; // console.log(`[${new Date().toISOString()}] No session ID specified, using first available connection: ${firstSessionId}`); // Update last activity time connectionInfo.lastActivityTime = Date.now(); // Handle message await this.handleClientMessage(connectionInfo.transport, req, res); } else { throw new Error('No active SSE connections'); } } catch (error) { // console.error(`[${new Date().toISOString()}] Failed to handle client message:`, error); res.status(500).json({ error: 'Failed to process message', message: error instanceof Error ? error.message : String(error), timestamp: Date.now() }); } }); // Error handling middleware app.use((err: Error, req: Request, res: Response, next: NextFunction) => { // console.error(`[${new Date().toISOString()}] Unhandled exception:`, err); res.status(500).json({ error: 'Internal server error', message: err.message, timestamp: Date.now() }); }); // Start Express server return new Promise((resolve) => { this.server = app.listen(port, () => { // console.log(`[${new Date().toISOString()}] MiniMax MCP SSE server started at: http://localhost:${port}`); // console.log(`- SSE connection endpoint: http://localhost:${port}/sse`); // console.log(`- Message handling endpoint: http://localhost:${port}${endpoint}`); // Set up process signal handlers this.setupSignalHandlers(); // Start connection monitoring this.startConnectionMonitoring(); resolve(); }); }); } catch (error) { // console.error(`[${new Date().toISOString()}] Failed to start SSE server:`, error); throw error; } } /** * Handle client messages with retry mechanism */ private async handleClientMessage(transport: SSEServerTransport, req: Request, res: Response, attempt = 1): Promise<void> { try { await transport.handlePostMessage(req, res, req.body); } catch (error) { if (attempt < MAX_RETRY_ATTEMPTS) { // console.warn(`[${new Date().toISOString()}] Failed to handle message, attempting retry (${attempt}/${MAX_RETRY_ATTEMPTS}):`, error); // Exponential backoff retry await new Promise(resolve => setTimeout(resolve, RETRY_DELAY * Math.pow(2, attempt - 1))); return this.handleClientMessage(transport, req, res, attempt + 1); } // console.error(`[${new Date().toISOString()}] Failed to handle message after ${MAX_RETRY_ATTEMPTS} attempts:`, error); throw error; } } /** * Start connection monitoring, periodically check and clean up inactive connections */ private startConnectionMonitoring(): void { // Check connection status every minute this.connectionMonitorInterval = setInterval(() => { const now = Date.now(); const inactiveThreshold = 5 * 60 * 1000; // 5 minutes of inactivity is considered timeout // console.log(`[${new Date().toISOString()}] Performing connection monitoring: ${this.connections.size} active connections`); for (const [sessionId, connectionInfo] of this.connections.entries()) { const inactiveDuration = now - connectionInfo.lastActivityTime; if (inactiveDuration > inactiveThreshold) { // console.warn(`[${new Date().toISOString()}] Detected inactive connection ${sessionId}, timed out after ${Math.round(inactiveDuration / 1000)} seconds, closing...`); this.closeConnection(sessionId); } } }, 60000); // Check every minute } /** * Close a single connection * @param sessionId Session ID */ private closeConnection(sessionId: string): void { const connectionInfo = this.connections.get(sessionId); if (!connectionInfo) return; try { // Clear heartbeat timer if (connectionInfo.heartbeatInterval) { clearInterval(connectionInfo.heartbeatInterval); } // Try to send close event try { const res = connectionInfo.transport['res']; // Access private property if (res && typeof res.write === 'function') { res.write(`event: server_shutdown\ndata: {"reason":"Connection closed","timestamp":${Date.now()}}\n\n`); res.end(); } } catch (error) { // console.error(`[${new Date().toISOString()}] Failed to send close event: ${sessionId}`, error); } // console.log(`[${new Date().toISOString()}] Connection closed: ${sessionId}`); } catch (error) { // console.error(`[${new Date().toISOString()}] Error closing connection: ${sessionId}`, error); } finally { // Always remove the connection from the Map this.connections.delete(sessionId); } } /** * Set up process signal handlers */ private setupSignalHandlers(): void { process.on('SIGTERM', async () => { // console.log(`[${new Date().toISOString()}] 接收到SIGTERM信号,准备关闭`); await this.closeAllConnections(); this.stopConnectionMonitoring(); this.server.close(() => { // console.log(`[${new Date().toISOString()}] 服务器已关闭`); process.exit(0); }); }); process.on('SIGINT', async () => { // console.log(`[${new Date().toISOString()}] 接收到SIGINT信号,准备关闭`); await this.closeAllConnections(); this.stopConnectionMonitoring(); if (this.server) { this.server.close(); } process.exit(0); }); } /** * Stop connection monitoring */ private stopConnectionMonitoring(): void { if (this.connectionMonitorInterval) { clearInterval(this.connectionMonitorInterval); this.connectionMonitorInterval = null; } } /** * Close all SSE connections */ private async closeAllConnections(): Promise<void> { // console.log(`[${new Date().toISOString()}] Closing all connections (${this.connections.size})`); // Get all session IDs const sessionIds = [...this.connections.keys()]; // Close each connection sequentially for (const sessionId of sessionIds) { try { // console.log(`[${new Date().toISOString()}] Attempting to close connection: ${sessionId}`); this.closeConnection(sessionId); } catch (error) { // console.error(`[${new Date().toISOString()}] Failed to close connection: ${sessionId}`, error); } } // Ensure connections map is empty this.connections.clear(); // console.log(`[${new Date().toISOString()}] All connections closed`); } /** * Get the MCP server instance * @returns McpServer instance */ public getMCPServer(): McpServer { return this.mcpServer; } /** * Get current configuration * @returns Current configuration */ public getConfig(): Config { return this.config; } /** * Get connection count * @returns Number of active connections */ public getConnectionCount(): number { return this.connections.size; } /** * Get all connection session IDs * @returns Array of session IDs */ public getSessionIds(): string[] { return [...this.connections.keys()]; } }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/MiniMax-AI/MiniMax-MCP-JS'

If you have feedback or need assistance with the MCP directory API, please join our Discord server