├── example.minimax-config.json ├── .editorconfig ├── .prettierrc ├── Dockerfile ├── tsconfig.json ├── src ├── schema │ └── index.ts ├── api │ ├── voice.ts │ ├── voice-design.ts │ ├── image.ts │ ├── voice-clone.ts │ ├── music.ts │ ├── video.ts │ └── tts.ts ├── exceptions │ └── index.ts ├── types │ └── index.ts ├── utils │ ├── audio.ts │ ├── file.ts │ └── api.ts ├── const │ └── index.ts ├── index.ts ├── services │ ├── index.ts │ └── media-service.ts ├── config │ └── ConfigManager.ts ├── mcp-rest-server.ts └── mcp-server.ts ├── .prettierignore ├── .env.example ├── .gitignore ├── LICENSE ├── smithery.yaml ├── .github └── ISSUE_TEMPLATE │ ├── Feature request.yml │ ├── Model Inquiry.yml │ ├── Bug Report for MCP.yml │ └── Bad case about the model.yml ├── package.json ├── README.zh-CN.md └── README.md /example.minimax-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "apiKey": "your_api_key_here", 3 | "apiHost": "https://api.minimax.chat", 4 | "basePath": "~/Desktop", 5 | "resourceMode": "url", 6 | "server": { 7 | "mode": "stdio", 8 | "port": 9593, 9 | "endpoint": "/rest" 10 | } 11 | } -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | max_line_length = 120 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | max_line_length = 0 14 | trim_trailing_whitespace = false -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "all", 3 | "tabWidth": 2, 4 | "semi": true, 5 | "singleQuote": true, 6 | "useTabs": false, 7 | "bracketSpacing": true, 8 | "arrowParens": "always", 9 | "jsxSingleQuote": false, 10 | "printWidth": 120, 11 | "endOfLine": "lf", 12 | "embeddedLanguageFormatting": "auto", 13 | "proseWrap": "always" 14 | } 15 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Generated by https://smithery.ai. See: https://smithery.ai/docs/build/project-config 2 | FROM node:20-alpine 3 | 4 | WORKDIR /app 5 | 6 | ENV API_KEY=dummy \ 7 | API_HOST=https://api.minimax.chat 8 | 9 | RUN npm config set registry https://registry.npmjs.org/ \ 10 | && npm install -g minimax-mcp-js@latest 11 | 12 | ENTRYPOINT ["minimax-mcp-js", "--mode=rest"] 13 | 14 | EXPOSE 3000 15 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "module": "Node16", 5 | "moduleResolution": "Node16", 6 | "outDir": "./build", 7 | "rootDir": "./src", 8 | "strict": true, 9 | "esModuleInterop": true, 10 | "skipLibCheck": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "resolveJsonModule": true 13 | }, 14 | "include": ["src/**/*"], 15 | "exclude": ["node_modules"] 16 | } 17 | -------------------------------------------------------------------------------- /src/schema/index.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { OUTPUT_DIRECTORY_DESCRIPTION } from "../const/index.js"; 3 | 4 | export const COMMON_REST_ARGUMENTS = [ 5 | { name: 'outputDirectory', description: OUTPUT_DIRECTORY_DESCRIPTION, required: false } 6 | ]; 7 | 8 | export const COMMON_REST_INPUT_SCHEMA_PROPERTIES = { 9 | outputDirectory: { type: 'string' } 10 | }; 11 | 12 | export const COMMON_PARAMETERS_SCHEMA = { 13 | outputDirectory: z.string().optional().describe(OUTPUT_DIRECTORY_DESCRIPTION), 14 | }; -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | /node_modules 3 | /.pnp 4 | .pnp.js 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /dist 11 | /build 12 | 13 | # misc 14 | .DS_Store 15 | *.pem 16 | 17 | # debug 18 | npm-debug.log* 19 | yarn-debug.log* 20 | yarn-error.log* 21 | 22 | # local env files 23 | .env.local 24 | .env.development.local 25 | .env.test.local 26 | .env.production.local 27 | 28 | # vercel 29 | .vercel 30 | 31 | # changelog 32 | CHANGELOG.md 33 | 34 | # game 35 | /src/constant/game/ 36 | 37 | /src/components/Game/ 38 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # MiniMax API Configuration 2 | MINIMAX_API_HOST=https://api.minimax.chat 3 | MINIMAX_API_KEY=your_api_key_here 4 | MINIMAX_RESOURCE_MODE=url 5 | MINIMAX_MCP_BASE_PATH=~/Desktop 6 | 7 | # Server Configuration 8 | # Transport mode: 'stdio' (default), 'rest', or 'sse' 9 | MINIMAX_TRANSPORT_MODE=stdio 10 | # Server port (for REST and SSE modes) 11 | MINIMAX_SERVER_PORT=9593 12 | # Server endpoint (for REST and SSE modes) 13 | MINIMAX_SERVER_ENDPOINT=/rest 14 | 15 | # Configuration file path (optional) 16 | # MINIMAX_CONFIG_PATH=./example.minimax-config.json 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | /node_modules 3 | /.pnp 4 | .pnp.js 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /dist 11 | /build 12 | 13 | # misc 14 | .DS_Store 15 | *.pem 16 | 17 | # debug 18 | npm-debug.log* 19 | yarn-debug.log* 20 | yarn-error.log* 21 | .pnpm-debug.log* 22 | 23 | # local env files 24 | .env.example 25 | .env.* 26 | !.env.example 27 | .env 28 | 29 | # typescript 30 | *.tsbuildinfo 31 | 32 | # IDEs and editors 33 | .idea/ 34 | .vscode/ 35 | *.swp 36 | *.swo 37 | 38 | # Logs 39 | logs 40 | *.log 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # macOS 49 | .AppleDouble 50 | .LSOverride 51 | ._* 52 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright 2025 MiniMax AI. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /smithery.yaml: -------------------------------------------------------------------------------- 1 | # Smithery configuration file: https://smithery.ai/docs/build/project-config 2 | 3 | startCommand: 4 | type: stdio 5 | commandFunction: 6 | # A JS function that produces the CLI command based on the given config to start the MCP on stdio. 7 | |- 8 | (config) => ({ command: 'npx', args: ['-y', 'minimax-mcp-js'], env: { MINIMAX_API_KEY: config.apiKey, MINIMAX_API_HOST: config.apiHost, MINIMAX_MCP_BASE_PATH: config.basePath, MINIMAX_RESOURCE_MODE: config.resourceMode } }) 9 | configSchema: 10 | # JSON Schema defining the configuration options for the MCP. 11 | type: object 12 | required: 13 | - apiKey 14 | properties: 15 | apiKey: 16 | type: string 17 | description: MiniMax API Key 18 | apiHost: 19 | type: string 20 | default: https://api.minimaxi.chat 21 | description: MiniMax API Host 22 | basePath: 23 | type: string 24 | default: /tmp 25 | description: Base path for output files 26 | resourceMode: 27 | type: string 28 | default: url 29 | description: Resource handling mode 30 | exampleConfig: 31 | apiKey: your_api_key_here 32 | apiHost: https://api.minimaxi.chat 33 | basePath: /tmp/minimax 34 | resourceMode: url 35 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Feature request.yml: -------------------------------------------------------------------------------- 1 | name: Feature Request 2 | description: Propose a new feature or enhancement for the project. 3 | title: "[request]: " 4 | labels: ["enhancement", "feature-request", "triage"] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | Thank you for suggesting a new feature! Please provide the following details to help us understand your proposal. 10 | 11 | - type: input 12 | id: feature-about 13 | attributes: 14 | label: Basic Information - Feature about 15 | description: "Please briefly describe the feature, including the type of use and the framework, e.g., support Minimax-M1 in Ollama." 16 | placeholder: "e.g., support Minimax-M1 in Ollama." 17 | validations: 18 | required: true 19 | 20 | - type: textarea 21 | id: proposal 22 | attributes: 23 | label: Proposal 24 | description: | 25 | Please describe the feature you have requested and the rationale behind it. 26 | The following template is recommended. Feel free to modify it as you needed. 27 | value: | 28 | #### Introduction 29 | I would like that ... 30 | 31 | #### Rational 32 | Implementation of this feature will help the following usecase: 33 | - ... 34 | - ... 35 | 36 | #### Anything else 37 | I find ... has this feature and xxx can serve as a reference for implementation. 38 | validations: 39 | required: true 40 | -------------------------------------------------------------------------------- /src/api/voice.ts: -------------------------------------------------------------------------------- 1 | import { MiniMaxAPI } from '../utils/api.js'; 2 | import { ListVoicesRequest } from '../types/index.js'; 3 | import { MinimaxRequestError } from '../exceptions/index.js'; 4 | 5 | export class VoiceAPI { 6 | private api: MiniMaxAPI; 7 | 8 | constructor(api: MiniMaxAPI) { 9 | this.api = api; 10 | } 11 | 12 | /** 13 | * List all available voices 14 | * @param request Request parameters 15 | * @returns Voice list information 16 | */ 17 | async listVoices(request: ListVoicesRequest = {}): Promise<{ systemVoices: string[], voiceCloneVoices: string[] }> { 18 | try { 19 | // Send request 20 | const response = await this.api.post('/v1/get_voice', { 21 | voice_type: request.voiceType || 'all' 22 | }); 23 | 24 | // Process response 25 | const systemVoices = response?.system_voice || []; 26 | const voiceCloneVoices = response?.voice_cloning || []; 27 | 28 | // Format voice information 29 | const systemVoiceList: string[] = []; 30 | const voiceCloneVoiceList: string[] = []; 31 | 32 | for (const voice of systemVoices) { 33 | systemVoiceList.push(`Name: ${voice.voice_name}, ID: ${voice.voice_id}`); 34 | } 35 | 36 | for (const voice of voiceCloneVoices) { 37 | voiceCloneVoiceList.push(`Name: ${voice.voice_name}, ID: ${voice.voice_id}`); 38 | } 39 | 40 | return { 41 | systemVoices: systemVoiceList, 42 | voiceCloneVoices: voiceCloneVoiceList 43 | }; 44 | } catch (error) { 45 | throw new MinimaxRequestError(`Failed to list voices: ${String(error)}`); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Model Inquiry.yml: -------------------------------------------------------------------------------- 1 | name: Model Inquiry 2 | description: Ask a question about the open source models. 3 | title: "[Inquiry]: " 4 | labels: ["question", "triage"] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | Thank you for reaching out! Please provide the following details to help us understand and address your inquiry about models. 10 | 11 | - type: input 12 | attributes: 13 | label: Basic Information - Models Used 14 | description: | 15 | Please list the model used, e.g., MiniMax-M1, speech-02-hd, etc. 16 | Our models can be referred at [HuggingFace](https://huggingface.co/MiniMaxAI) or [the official site](https://www.minimax.io/platform_overview). 17 | placeholder: "ex: MiniMax-M1" 18 | validations: 19 | required: true 20 | 21 | - type: checkboxes 22 | id: problem-validation 23 | attributes: 24 | label: Is this information known and solvable? 25 | options: 26 | - label: "I have checked [Minimax documentation](https://www.minimax.io/platform_overview) and found no solution." 27 | required: true 28 | - label: "I have searched existing issues and found no duplicates." 29 | required: true 30 | 31 | 32 | - type: textarea 33 | id: detailed-description 34 | attributes: 35 | label: Description 36 | description: "Please describe your question in detail here. If available, please paste relevant screenshots directly into this box." 37 | placeholder: | 38 | - Your detailed question or issue description. 39 | - Relevant context or background information. 40 | - (Paste screenshots directly below this text) 41 | validations: 42 | required: true 43 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "minimax-mcp-js", 3 | "version": "0.0.17", 4 | "description": "Official MiniMax Model Context Protocol (MCP) JavaScript implementation that provides seamless integration with MiniMax's powerful AI capabilities including image generation, video generation, text-to-speech, and voice cloning APIs.", 5 | "main": "build/index.js", 6 | "type": "module", 7 | "bin": { 8 | "minimax-mcp-js": "./build/index.js" 9 | }, 10 | "scripts": { 11 | "build": "tsc && chmod 755 build/index.js", 12 | "start": "node build/index.js", 13 | "dev": "tsc -w", 14 | "format": "prettier --write \"src/**/*.ts\"", 15 | "lint": "prettier --check \"src/**/*.ts\"", 16 | "prepare": "pnpm run build", 17 | "pretest": "pnpm run build", 18 | "inspector": "npx @modelcontextprotocol/inspector build/index.js" 19 | }, 20 | "keywords": [ 21 | "mcp", 22 | "minimax", 23 | "ai", 24 | "image-generation", 25 | "video-generation", 26 | "music-generation", 27 | "text-to-speech", 28 | "tts" 29 | ], 30 | "author": "Mark Yang ", 31 | "license": "MIT", 32 | "repository": { 33 | "type": "git", 34 | "url": "git+https://github.com/MiniMax-AI/MiniMax-MCP-JS.git" 35 | }, 36 | "bugs": { 37 | "url": "https://github.com/MiniMax-AI/MiniMax-MCP-JS/issues" 38 | }, 39 | "homepage": "https://github.com/MiniMax-AI/MiniMax-MCP-JS#readme", 40 | "dependencies": { 41 | "@chatmcp/sdk": "^1.0.5", 42 | "@modelcontextprotocol/sdk": "^1.7.0", 43 | "axios": "^1.8.4", 44 | "cors": "^2.8.5", 45 | "dotenv": "^16.5.0", 46 | "express": "^4.18.2", 47 | "yargs": "18.0.0-candidate.4", 48 | "zod": "^3.24.2" 49 | }, 50 | "devDependencies": { 51 | "@types/cors": "^2.8.17", 52 | "@types/express": "^5.0.1", 53 | "@types/node": "^22.14.1", 54 | "@types/yargs": "^17.0.33", 55 | "prettier": "^3.2.1", 56 | "typescript": "^5.8.3" 57 | }, 58 | "engines": { 59 | "node": ">=20.0.0", 60 | "pnpm": ">=8.0.0" 61 | }, 62 | "files": [ 63 | "build", 64 | "README.md", 65 | "README.zh-CN.md" 66 | ], 67 | "publishConfig": { 68 | "access": "public" 69 | } 70 | } -------------------------------------------------------------------------------- /src/exceptions/index.ts: -------------------------------------------------------------------------------- 1 | import { AxiosError } from 'axios'; 2 | 3 | /** 4 | * Base error class for Minimax API errors 5 | */ 6 | export class MinimaxError extends Error { 7 | constructor(message: string) { 8 | super(message); 9 | this.name = 'MinimaxError'; 10 | } 11 | } 12 | 13 | /** 14 | * Error class for authentication issues 15 | */ 16 | export class MinimaxAuthError extends MinimaxError { 17 | constructor(message: string) { 18 | super(message); 19 | this.name = 'MinimaxAuthError'; 20 | } 21 | } 22 | 23 | /** 24 | * Error class for request issues 25 | */ 26 | export class MinimaxRequestError extends MinimaxError { 27 | constructor(message: string) { 28 | super(message); 29 | this.name = 'MinimaxRequestError'; 30 | } 31 | } 32 | 33 | /** 34 | * Error class for parameter issues 35 | */ 36 | export class MinimaxParameterError extends MinimaxError { 37 | constructor(message: string) { 38 | super(message); 39 | this.name = 'MinimaxParameterError'; 40 | } 41 | } 42 | 43 | /** 44 | * Error class for resource issues (files, directories) 45 | */ 46 | export class MinimaxResourceError extends MinimaxError { 47 | constructor(message: string) { 48 | super(message); 49 | this.name = 'MinimaxResourceError'; 50 | } 51 | } 52 | 53 | /** 54 | * Create an error from an Axios error 55 | * @param error Axios error 56 | * @returns Minimax error 57 | */ 58 | export function createApiErrorFromAxiosError(error: AxiosError): MinimaxError { 59 | if (error.response) { 60 | // The request was made and the server responded with a status code 61 | // that falls out of the range of 2xx 62 | const status = error.response.status; 63 | const data = error.response.data as any; 64 | 65 | if (status === 401 || status === 403) { 66 | return new MinimaxAuthError(`Authentication error: ${data?.message || status}`); 67 | } 68 | 69 | return new MinimaxRequestError(`API Error (${status}): ${data?.message || 'Unknown error'}`); 70 | } else if (error.request) { 71 | // The request was made but no response was received 72 | return new MinimaxRequestError('No response received from server'); 73 | } else { 74 | // Something happened in setting up the request 75 | return new MinimaxRequestError(`Request configuration error: ${error.message}`); 76 | } 77 | } -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | export interface BaseToolRequest { 2 | outputDirectory?: string; 3 | } 4 | 5 | export interface TTSRequest extends BaseToolRequest { 6 | text: string; 7 | model?: string; 8 | voiceId?: string; 9 | speed?: number; 10 | vol?: number; 11 | pitch?: number; 12 | emotion?: string; 13 | format?: string; 14 | sampleRate?: number; 15 | bitrate?: number; 16 | channel?: number; 17 | latexRead?: boolean; 18 | pronunciationDict?: string[]; 19 | stream?: boolean; 20 | languageBoost?: string; 21 | subtitleEnable?: boolean; 22 | outputFormat?: string; 23 | outputFile?: string; 24 | } 25 | 26 | export interface ImageGenerationRequest extends BaseToolRequest { 27 | prompt: string; 28 | model?: string; 29 | aspectRatio?: string; 30 | n?: number; 31 | promptOptimizer?: boolean; 32 | outputFile?: string; 33 | subjectReference?: string; 34 | } 35 | 36 | export interface VideoGenerationRequest extends BaseToolRequest { 37 | prompt: string; 38 | model?: string; 39 | duration?: number; 40 | fps?: number; 41 | firstFrameImage?: string; 42 | outputFile?: string; 43 | resolution?: string; 44 | asyncMode?: boolean; 45 | } 46 | 47 | export interface VideoGenerationQueryRequest extends BaseToolRequest { 48 | taskId: string; 49 | } 50 | 51 | export interface VoiceCloneRequest extends BaseToolRequest { 52 | audioFile: string; 53 | voiceId: string; 54 | text?: string; 55 | name?: string; 56 | description?: string; 57 | isUrl?: boolean; 58 | } 59 | 60 | export interface ListVoicesRequest { 61 | voiceType?: string; 62 | } 63 | 64 | export interface PlayAudioRequest { 65 | inputFilePath: string; 66 | isUrl?: boolean; 67 | } 68 | 69 | export interface MusicGenerationRequest extends BaseToolRequest { 70 | prompt: string; 71 | lyrics: string; 72 | sampleRate?: number; 73 | bitrate?: number; 74 | format?: string; 75 | channel?: number; 76 | outputFormat?: string; 77 | } 78 | 79 | export interface VoiceDesignRequest extends BaseToolRequest { 80 | prompt: string; 81 | previewText: string; 82 | voiceId?: string; 83 | } 84 | 85 | export type TransportMode = 'stdio' | 'rest' | 'sse'; 86 | 87 | export interface ServerOptions { 88 | port?: number; 89 | endpoint?: string; 90 | mode?: TransportMode; 91 | } 92 | 93 | export interface Config { 94 | apiKey: string; 95 | basePath?: string; 96 | apiHost?: string; 97 | resourceMode?: string; 98 | server?: ServerOptions; 99 | } -------------------------------------------------------------------------------- /src/utils/audio.ts: -------------------------------------------------------------------------------- 1 | import { spawn } from 'child_process'; 2 | import * as os from 'os'; 3 | import * as fs from 'fs'; 4 | import * as path from 'path'; 5 | import { PlayAudioRequest } from '../types/index.js'; 6 | import { MinimaxRequestError } from '../exceptions/index.js'; 7 | import { processInputFile } from './file.js'; 8 | import * as requests from 'axios'; 9 | 10 | /** 11 | * Play audio file 12 | * @param request Play request 13 | * @returns Success message 14 | */ 15 | export async function playAudio(request: PlayAudioRequest): Promise { 16 | try { 17 | let data: Buffer; 18 | 19 | if (request.isUrl) { 20 | // If URL, download audio data 21 | const response = await requests.default.get(request.inputFilePath, { responseType: 'arraybuffer' }); 22 | data = Buffer.from(response.data); 23 | } else { 24 | // If local file, read file 25 | const filePath = processInputFile(request.inputFilePath); 26 | data = fs.readFileSync(filePath); 27 | } 28 | 29 | // Save to temporary file 30 | const tempDir = os.tmpdir(); 31 | const tempFile = path.join(tempDir, `audio_${Date.now()}.mp3`); 32 | fs.writeFileSync(tempFile, data); 33 | 34 | // Play audio based on operating system 35 | const platform = os.platform(); 36 | let player; 37 | 38 | if (platform === 'darwin') { 39 | // macOS 40 | player = spawn('afplay', [tempFile]); 41 | } else if (platform === 'win32') { 42 | // Windows 43 | player = spawn('powershell', ['-c', `(New-Object Media.SoundPlayer "${tempFile}").PlaySync()`]); 44 | } else { 45 | // Linux/Unix 46 | player = spawn('play', [tempFile]); 47 | } 48 | 49 | // Wait for playback to complete 50 | return new Promise((resolve, reject) => { 51 | player.on('close', (code) => { 52 | // Delete temporary file 53 | fs.unlinkSync(tempFile); 54 | 55 | if (code === 0) { 56 | resolve(`Successfully played audio: ${request.inputFilePath}`); 57 | } else { 58 | reject(new Error(`Failed to play audio, error code: ${code}`)); 59 | } 60 | }); 61 | 62 | player.on('error', (err) => { 63 | // Delete temporary file 64 | try { 65 | fs.unlinkSync(tempFile); 66 | } catch (e) { 67 | // Ignore deletion failure 68 | } 69 | 70 | reject(new Error(`Failed to start audio player: ${err.message}`)); 71 | }); 72 | }); 73 | } catch (error) { 74 | throw new MinimaxRequestError(`Failed to play audio: ${String(error)}`); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/api/voice-design.ts: -------------------------------------------------------------------------------- 1 | import { MiniMaxAPI } from '../utils/api.js'; 2 | import { MinimaxRequestError } from '../exceptions/index.js'; 3 | import { VoiceDesignRequest } from '../types/index.js'; 4 | import { ERROR_PROMPT_REQUIRED, ERROR_PREVIEW_TEXT_REQUIRED } from '../const/index.js'; 5 | import { buildOutputFile } from '../utils/file.js'; 6 | import * as path from 'path'; 7 | import * as fs from 'fs'; 8 | 9 | export class VoiceDesignAPI { 10 | private api: MiniMaxAPI; 11 | 12 | constructor(api: MiniMaxAPI) { 13 | this.api = api; 14 | } 15 | 16 | async voiceDesign(request: VoiceDesignRequest): Promise { 17 | // Validate required parameters 18 | if (!request.prompt || request.prompt.trim() === '') { 19 | throw new MinimaxRequestError(ERROR_PROMPT_REQUIRED); 20 | } 21 | if (!request.previewText || request.previewText.trim() === '') { 22 | throw new MinimaxRequestError(ERROR_PREVIEW_TEXT_REQUIRED); 23 | } 24 | 25 | // Process output file 26 | const textPrefix = request.prompt.substring(0, 20).replace(/[^\w]/g, '_'); 27 | const fileName = `voice_design_${textPrefix}_${Date.now()}`; 28 | const outputFile = buildOutputFile(fileName, request.outputDirectory, 'mp3'); 29 | 30 | // Prepare request data 31 | const requestData: Record = { 32 | prompt: request.prompt, 33 | preview_text: request.previewText, 34 | voice_id: request.voiceId, 35 | }; 36 | 37 | try { 38 | // Send request 39 | const response = await this.api.post('/v1/voice_design', requestData); 40 | 41 | // Process response 42 | const trialAudioData = response?.trial_audio; 43 | const voiceId = response?.voice_id; 44 | 45 | if (!trialAudioData) { 46 | throw new MinimaxRequestError('Could not get audio data from response'); 47 | } 48 | 49 | // decode and save file 50 | try { 51 | // Convert hex string to binary 52 | const audioBuffer = Buffer.from(trialAudioData, 'hex'); 53 | 54 | // Ensure output directory exists 55 | const outputDir = path.dirname(outputFile); 56 | if (!fs.existsSync(outputDir)) { 57 | fs.mkdirSync(outputDir, { recursive: true }); 58 | } 59 | 60 | // Write to file 61 | fs.writeFileSync(outputFile, audioBuffer); 62 | 63 | return { 64 | voiceId, 65 | outputFile, 66 | }; 67 | } catch (error) { 68 | throw new MinimaxRequestError(`Failed to save audio file: ${String(error)}`); 69 | } 70 | } catch (err) { 71 | throw err; 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/const/index.ts: -------------------------------------------------------------------------------- 1 | // Environment variable names 2 | export const ENV_MINIMAX_API_KEY = 'MINIMAX_API_KEY'; 3 | export const ENV_MINIMAX_API_HOST = 'MINIMAX_API_HOST'; 4 | export const ENV_MINIMAX_MCP_BASE_PATH = 'MINIMAX_MCP_BASE_PATH'; 5 | export const ENV_RESOURCE_MODE = 'MINIMAX_RESOURCE_MODE'; 6 | export const ENV_TRANSPORT_MODE = 'MINIMAX_TRANSPORT_MODE'; 7 | export const ENV_SERVER_PORT = 'MINIMAX_SERVER_PORT'; 8 | export const ENV_SERVER_ENDPOINT = 'MINIMAX_SERVER_ENDPOINT'; 9 | export const ENV_CONFIG_PATH = 'MINIMAX_CONFIG_PATH'; 10 | 11 | // Resource modes 12 | export const RESOURCE_MODE_URL = 'url'; 13 | export const RESOURCE_MODE_LOCAL = 'local'; 14 | export const RESOURCE_MODE_BASE64 = 'base64'; 15 | 16 | // Transport modes 17 | export const TRANSPORT_MODE_STDIO = 'stdio'; 18 | export const TRANSPORT_MODE_REST = 'rest'; 19 | export const TRANSPORT_MODE_SSE = 'sse'; 20 | 21 | // Default values 22 | export const DEFAULT_API_HOST = 'https://api.minimax.chat'; 23 | export const DEFAULT_SPEECH_MODEL = 'speech-02-hd'; 24 | export const DEFAULT_T2I_MODEL = 'image-01'; 25 | export const DEFAULT_T2V_MODEL = 'T2V-01'; 26 | export const DEFAULT_VOICE_ID = 'male-qn-qingse'; 27 | export const DEFAULT_EMOTION = 'happy'; 28 | export const DEFAULT_FORMAT = 'mp3'; 29 | export const DEFAULT_SPEED = 1.0; 30 | export const DEFAULT_VOLUME = 1.0; 31 | export const DEFAULT_PITCH = 0; 32 | export const DEFAULT_BITRATE = 128000; 33 | export const DEFAULT_CHANNEL = 1; 34 | export const DEFAULT_SAMPLE_RATE = 32000; 35 | export const DEFAULT_LANGUAGE_BOOST = 'auto'; 36 | export const DEFAULT_TRANSPORT_MODE = TRANSPORT_MODE_STDIO; 37 | export const DEFAULT_SERVER_PORT = 9593; 38 | export const DEFAULT_SERVER_ENDPOINT = '/rest'; 39 | export const DEFAULT_MUSIC_MODEL = 'music-1.5'; 40 | export const DEFAULT_VIDEO_MODEL = 'MiniMax-Hailuo-02'; 41 | 42 | // Error messages 43 | export const ERROR_API_KEY_REQUIRED = 'API_KEY environment variable is required'; 44 | export const ERROR_API_HOST_REQUIRED = 'API_HOST is required'; 45 | export const ERROR_TEXT_REQUIRED = 'Text is required for text-to-speech conversion.'; 46 | export const ERROR_PROMPT_REQUIRED = 'Prompt is required for generation.'; 47 | export const ERROR_AUDIO_FILE_REQUIRED = 'Audio file is required for voice cloning.'; 48 | export const ERROR_LYRICS_REQUIRED = 'Lyrics are required for music generation.'; 49 | export const ERROR_PREVIEW_TEXT_REQUIRED = 'Preview text is required for voice design.'; 50 | 51 | // Default Values 52 | export const VALID_VIDEO_MODELS = ['T2V-01', 'T2V-01-Director', 'I2V-01', 'I2V-01-Director', 'I2V-01-live', 'S2V-01', "MiniMax-Hailuo-02"]; 53 | export const VALID_IMAGE_MODELS = ['image-01']; 54 | 55 | 56 | // Default Description 57 | export const OUTPUT_DIRECTORY_DESCRIPTION = 'The directory to save the output file. `outputDirectory` is relative to `MINIMAX_MCP_BASE_PATH` (or `basePath` in config). The final save path is `${basePath}/${outputDirectory}`. For example, if `MINIMAX_MCP_BASE_PATH=~/Desktop` and `outputDirectory=workspace`, the output will be saved to `~/Desktop/workspace/`'; 58 | 59 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Bug Report for MCP.yml: -------------------------------------------------------------------------------- 1 | name: Bug Report for MCP&API 2 | description: Report a bug related to MCP and API tasks to help us reproduce and fix the problem. 3 | title: "[Bug for MCP&API]: " 4 | labels: ["bug", "triage"] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | Thank you for contributing to our project by reporting a bug! To help us understand and resolve the issue as quickly as possible, please provide the following details. 10 | 11 | - type: input 12 | attributes: 13 | label: Basic Information - Models Used 14 | description: | 15 | Please list the model used, e.g., MiniMax-M1, speech-02-hd, etc. 16 | Our models can be referred at [HuggingFace](https://huggingface.co/MiniMaxAI) or [the official site](https://www.minimax.io/platform_overview). 17 | placeholder: "ex: MiniMax-M1" 18 | validations: 19 | required: true 20 | 21 | - type: input 22 | id: scenario-description 23 | attributes: 24 | label: Basic Information - Scenario Description 25 | description: | 26 | Please briefly describe the scenario, including the framework or the platform, 27 | placeholder: "ex: Minimax-M1 cannot be called as MCP tools. " 28 | validations: 29 | required: false 30 | 31 | - type: checkboxes 32 | id: problem-validation 33 | attributes: 34 | label: Is this bug known and solvable? 35 | options: 36 | - label: "I have followed the GitHub READMEs for [`Minimax-MCP`](https://github.com/MiniMax-AI/MiniMax-MCP) and [`Minimax-MCP-JS`](https://github.com/MiniMax-AI/MiniMax-MCP-JS)." 37 | required: true 38 | - label: "I have checked the [official Minimax documentation](https://www.minimax.io/platform_overview) and [existing GitHub issues](https://github.com/MiniMax-AI/MiniMax-MCP/issues),but found no solution." 39 | required: true 40 | 41 | - type: textarea 42 | attributes: 43 | label: Information about environment 44 | description: | 45 | Please provide information about you environment, 46 | e.g., the software versions and the information on the OS, GPUs, python packages(from pip list) if available. 47 | placeholder: 48 | "For example: 49 | - OS: Ubuntu 24.04 50 | - Python: Python 3.11 51 | - PyTorch: 2.6.0+cu124" 52 | 53 | validations: 54 | required: true 55 | 56 | - type: input 57 | id: trace-id 58 | attributes: 59 | label: Trace-ID in the request head 60 | description: "Please copy and paste the trace-ID of the problematic request." 61 | validations: 62 | required: true 63 | 64 | - type: textarea 65 | attributes: 66 | label: Description 67 | description: | 68 | Please **describe the bug** you have encountered when using the MCP tools or API, and **paste the screenshots** of the error or unexpected behaviour here. 69 | The following template is recommended. 70 | Feel free to modify as you needed. 71 | value: | 72 | #### Steps to reproduce 73 | 74 | This happens to Minimax_M1 and xxx. 75 | The bug can be reproduced with the following steps: 76 | 1. ... 77 | 2. ... 78 | 79 | The following example input & output can be used: 80 | ``` 81 | system: ... 82 | user: ... 83 | ... 84 | ``` 85 | 86 | #### Expected results 87 | 88 | The results are expected to be ... 89 | 90 | #### Actual behaviours 91 | 92 | The actual outputs are as follows: ... 93 | 94 | #### Error logs 95 | 96 | The error logs are as follows: ... 97 | 98 | ### The screenshots are as belows: 99 | validations: 100 | required: true 101 | -------------------------------------------------------------------------------- /src/utils/file.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import * as fs from 'fs'; 3 | import * as os from 'os'; 4 | import { MinimaxResourceError } from '../exceptions/index.js'; 5 | import { ConfigManager } from '../config/ConfigManager.js'; 6 | 7 | /** 8 | * Expand home directory if path starts with ~ 9 | * @param filepath Path that might contain ~ 10 | * @returns Expanded path 11 | */ 12 | function expandHomeDir(filepath: string): string { 13 | if (filepath.startsWith('~/')) { 14 | return path.join(os.homedir(), filepath.slice(2)); 15 | } 16 | return filepath; 17 | } 18 | 19 | /** 20 | * Build output path 21 | * @param dir Output directory 22 | * @returns Complete path 23 | */ 24 | export function buildOutputPath(dir?: string): string { 25 | const BASE_PATH = expandHomeDir(ConfigManager.getConfig().basePath!); 26 | const outputPath = dir ? path.join(BASE_PATH, dir) : BASE_PATH; 27 | 28 | if (!fs.existsSync(outputPath)) { 29 | try { 30 | fs.mkdirSync(outputPath, { recursive: true }); 31 | } catch (err) { 32 | throw new MinimaxResourceError(`Cannot create output directory: ${outputPath}, dir: ${dir}, BASE_PATH: ${BASE_PATH}`); 33 | } 34 | } 35 | 36 | return outputPath; 37 | } 38 | 39 | /** 40 | * Sanitize filename to remove invalid characters 41 | * @param filename Filename to sanitize 42 | * @returns Sanitized filename 43 | */ 44 | function sanitizeFilename(filename: string): string { 45 | // Remove invalid characters 46 | return filename.replace(/[<>:"/\\|?*\x00-\x1F]/g, '_'); 47 | } 48 | 49 | /** 50 | * Build complete output file path 51 | * @param filename Filename 52 | * @param dir Directory 53 | * @param ext Extension (without dot) 54 | * @param useRandomSuffix Whether to use random suffix 55 | * @returns Complete file path 56 | */ 57 | export function buildOutputFile(filename: string, dir?: string, ext?: string, useRandomSuffix?: boolean): string { 58 | // Sanitize the filename first 59 | filename = sanitizeFilename(filename); 60 | 61 | if (useRandomSuffix) { 62 | const rnd = Math.random().toString(36).slice(2, 8); 63 | filename = `${filename}_${rnd}`; 64 | } 65 | 66 | if (ext && !filename.endsWith(`.${ext}`)) { 67 | ext = sanitizeFilename(ext); 68 | filename = `${filename}.${ext}`; 69 | } 70 | 71 | const folder = buildOutputPath(dir); 72 | return path.join(folder, filename); 73 | } 74 | 75 | /** 76 | * Process input file, check if it exists 77 | * @param filePath File path 78 | * @returns Complete file path 79 | */ 80 | export function processInputFile(filePath: string): string { 81 | // First expand home directory if present 82 | filePath = expandHomeDir(filePath); 83 | 84 | // Resolve to absolute path 85 | const resolved = path.isAbsolute(filePath) 86 | ? filePath 87 | : path.resolve(process.cwd(), filePath); 88 | 89 | // Normalize and sanitize path 90 | const normalized = path.normalize(resolved); 91 | 92 | // Prevent directory traversal 93 | if (!normalized.startsWith(process.cwd()) && !path.isAbsolute(filePath)) { 94 | throw new MinimaxResourceError(`Invalid file path: ${normalized} (directory traversal not allowed)`); 95 | } 96 | 97 | // Check if file exists 98 | if (!fs.existsSync(normalized)) { 99 | throw new MinimaxResourceError(`File does not exist: ${normalized}`); 100 | } 101 | 102 | // Check if it's a file 103 | if (!fs.statSync(normalized).isFile()) { 104 | throw new MinimaxResourceError(`Not a file: ${normalized}`); 105 | } 106 | 107 | return normalized; 108 | } 109 | 110 | /** 111 | * Generate timestamp filename 112 | * @param prefix Prefix 113 | * @param ext Extension (without dot) 114 | * @returns Timestamped filename 115 | */ 116 | export function generateTimestampFilename(prefix: string, ext: string): string { 117 | const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); 118 | return `${prefix}-${timestamp}.${ext}`; 119 | } 120 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import fs from 'fs'; 3 | import path from 'path'; 4 | import { fileURLToPath } from 'url'; 5 | import yargs from 'yargs'; 6 | import { hideBin } from 'yargs/helpers'; 7 | import { MCPServer } from './mcp-server.js'; 8 | import { MCPSSEServer } from './mcp-sse-server.js'; 9 | import { MCPRestServer } from './mcp-rest-server.js'; 10 | import { ConfigManager } from './config/ConfigManager.js'; 11 | import { 12 | DEFAULT_TRANSPORT_MODE, 13 | TRANSPORT_MODE_REST, 14 | TRANSPORT_MODE_SSE, 15 | TRANSPORT_MODE_STDIO, 16 | } from './const/index.js'; 17 | import type { Config, TransportMode } from './types/index.js'; 18 | 19 | export async function startMiniMaxMCP(customConfig?: Partial): Promise { 20 | try { 21 | const config = ConfigManager.getConfig(customConfig); 22 | const mode = config.server?.mode || DEFAULT_TRANSPORT_MODE; 23 | // console.log(`[${new Date().toISOString()}] Using transport mode: ${mode}`); 24 | 25 | if (mode === TRANSPORT_MODE_REST) { 26 | config.server = config.server || {}; 27 | config.server.port = config.server.port || 3000; 28 | // console.log(`[WARN] No --port specified for REST; defaulting to ${config.server.port}`); 29 | } 30 | 31 | switch (mode) { 32 | case TRANSPORT_MODE_REST: 33 | // console.log(`[${new Date().toISOString()}] Starting REST server`); 34 | return new MCPRestServer(config).start(); 35 | case TRANSPORT_MODE_SSE: 36 | // console.log(`[${new Date().toISOString()}] Starting SSE server`); 37 | return new MCPSSEServer(config).start(); 38 | case TRANSPORT_MODE_STDIO: 39 | default: 40 | // console.log(`[${new Date().toISOString()}] Starting STDIO server`); 41 | return new MCPServer(config).start(); 42 | } 43 | } catch (err) { 44 | // console.error(`[${new Date().toISOString()}] Failed to start server:`, err); 45 | process.exit(1); 46 | } 47 | } 48 | 49 | function isCLIEntry(): boolean { 50 | if (typeof (import.meta as any).main === 'boolean') { 51 | return (import.meta as any).main; 52 | } 53 | 54 | try { 55 | const invoked = process.argv[1] ? fs.realpathSync(process.argv[1]) : ''; 56 | const self = fs.realpathSync(fileURLToPath(import.meta.url)); 57 | if (invoked === self) return true; 58 | } catch { 59 | /* ignore */ 60 | } 61 | 62 | const invokedName = process.argv[1] ? path.basename(process.argv[1]) : ''; 63 | const selfName = path.basename(fileURLToPath(import.meta.url)); 64 | 65 | if (invokedName === selfName) return true; // node build/index.js 66 | if (invokedName === 'minimax-mcp-js') return true; // global bin 67 | 68 | return false; 69 | } 70 | 71 | if (isCLIEntry()) { 72 | const argv = yargs(hideBin(process.argv)) 73 | .option('mode', { 74 | alias: 'm', 75 | type: 'string', 76 | default: DEFAULT_TRANSPORT_MODE, 77 | describe: 'transport mode (rest | sse | stdio)', 78 | }) 79 | .option('port', { 80 | alias: 'p', 81 | type: 'number', 82 | default: 3000, 83 | describe: 'port for REST server (only applies when --mode=rest)', 84 | }) 85 | .help() 86 | .parseSync(); 87 | 88 | const customCfg: Partial = { 89 | server: { mode: argv.mode as TransportMode }, 90 | }; 91 | 92 | if (argv.port) { 93 | customCfg.server!.port = argv.port; 94 | } 95 | 96 | startMiniMaxMCP(customCfg).catch((err) => { 97 | // console.error(err); 98 | process.exit(1); 99 | }); 100 | } 101 | 102 | export * from './types/index.js'; 103 | export * from './utils/api.js'; 104 | export * from './api/tts.js'; 105 | export * from './api/image.js'; 106 | export * from './api/video.js'; 107 | export * from './api/voice-clone.js'; 108 | export * from './api/voice.js'; 109 | export * from './exceptions/index.js'; 110 | export * from './const/index.js'; 111 | export { MCPServer, MCPSSEServer, MCPRestServer, ConfigManager }; 112 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Bad case about the model.yml: -------------------------------------------------------------------------------- 1 | name: Bad Case Report of the model 2 | description: Report a bug related to the model to help us reproduce and fix the problem. 3 | title: "[BadCase about the model]: " 4 | 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | Thank you for contributing to our project by reporting a bad case! To help us understand and resolve the issue as quickly as possible, please provide the following details. 10 | 11 | - type: input 12 | id: models-used 13 | attributes: 14 | label: Basic Information - Models Used 15 | description: | 16 | Please list the model used, e.g., MiniMax-M1, speech-02-hd, etc. 17 | (Note: You can refer to our models at [HuggingFace](https://huggingface.co/MiniMaxAI) or [the official site](https://www.minimax.io/platform_overview) for more details.) 18 | placeholder: "ex: MiniMax-M1" 19 | validations: 20 | required: true 21 | 22 | - type: input 23 | id: scenario-description 24 | attributes: 25 | label: Basic Information - Scenario Description 26 | description: | 27 | Please briefly describe the scenario, including the framework or the platform. 28 | placeholder: "ex: Minimax-M1 return the error related to xxx." 29 | validations: 30 | required: false 31 | 32 | - type: checkboxes 33 | id: problem-validation 34 | attributes: 35 | label: Is this badcase known and solvable? 36 | options: 37 | - label: "I have followed the [GitHub README](https://github.com/MiniMax-AI) of the model and found no duplicates in existing issues." 38 | required: true 39 | - label: "I have checked [Minimax documentation](https://www.minimax.io/platform_overview) and found no solution." 40 | required: true 41 | 42 | - type: textarea 43 | id: environment-info 44 | attributes: 45 | label: Information about environment 46 | description: | 47 | (Include software versions, OS, GPUs if applicable) 48 | placeholder: | 49 | For example: 50 | - OS: Ubuntu 24.04 51 | - Python: Python 3.11 52 | - PyTorch: 2.6.0+cu124 53 | validations: 54 | required: true 55 | 56 | - type: textarea 57 | id: call-execution-info # Consolidated field for call type and details 58 | attributes: 59 | label: Call & Execution Information 60 | description: | 61 | Please describe how you are interacting with the model and provide the relevant details in the box below: 62 | **Call Type**: (e.g., API Call, Deployment Call) 63 | **If API Call**: Please provide the `trace-ID` of the problematic request. 64 | **If Deployment Call**: Please provide the command used for deployment or inference. 65 | placeholder: | 66 | # Example for API Call: 67 | Call Type: API Call 68 | Trace-ID: abcdef1234567890 69 | 70 | # Example for Deployment Call: 71 | Call Type: Deployment Call 72 | Deployment Command: python run_inference.py --model my_model --config config.yaml 73 | validations: 74 | required: true 75 | 76 | - type: textarea 77 | id: description-of-bug 78 | attributes: 79 | label: Description 80 | description: | 81 | Please **describe the bad case** you have encountered and **paste the screenshots** if available. 82 | The following template is recommended (modify as needed): 83 | value: | 84 | ### Steps to reproduce 85 | The bug can be reproduced with the following steps: 86 | 1. ... 87 | 2. ... 88 | 89 | ### Expected behavior 90 | The results are expected to be: ... 91 | 92 | ### Actual behavior 93 | The actual outputs are as follows: ... 94 | 95 | ### Error logs 96 | The error logs are as follows: 97 | ``` 98 | # Paste the related screenshots here 99 | ``` 100 | validations: 101 | required: true 102 | -------------------------------------------------------------------------------- /src/services/index.ts: -------------------------------------------------------------------------------- 1 | import { MiniMaxAPI } from '../utils/api.js'; 2 | import { Config } from '../types/index.js'; 3 | 4 | /** 5 | * Base service interface 6 | */ 7 | export interface ServiceInterface { 8 | getServiceName(): string; 9 | initialize(config: Config): Promise; 10 | } 11 | 12 | /** 13 | * Base service abstract class, all services inherit from this class 14 | */ 15 | export abstract class BaseService implements ServiceInterface { 16 | protected api: MiniMaxAPI; 17 | protected config: Config; 18 | protected initialized: boolean = false; 19 | protected serviceName: string; 20 | 21 | /** 22 | * Create service instance 23 | * @param api API instance 24 | * @param serviceName Service name 25 | */ 26 | constructor(api: MiniMaxAPI, serviceName: string) { 27 | this.api = api; 28 | this.serviceName = serviceName; 29 | this.config = {} as Config; // Initialize as empty object, will be set in initialize 30 | } 31 | 32 | /** 33 | * Get service name 34 | * @returns Service name 35 | */ 36 | public getServiceName(): string { 37 | return this.serviceName; 38 | } 39 | 40 | /** 41 | * Initialize service 42 | * @param config Configuration 43 | */ 44 | public async initialize(config: Config): Promise { 45 | this.config = config; 46 | this.initialized = true; 47 | } 48 | 49 | /** 50 | * Check if service is initialized 51 | * @throws Error when not initialized 52 | */ 53 | protected checkInitialized(): void { 54 | if (!this.initialized) { 55 | throw new Error(`服务 ${this.serviceName} 尚未初始化`); 56 | } 57 | } 58 | 59 | /** 60 | * Update API instance 61 | * @param api New API instance 62 | */ 63 | public updateApi(api: MiniMaxAPI): void { 64 | this.api = api; 65 | } 66 | } 67 | 68 | /** 69 | * Service manager, responsible for managing and accessing all services 70 | */ 71 | export class ServiceManager { 72 | private static instance: ServiceManager; 73 | private services: Map = new Map(); 74 | private api: MiniMaxAPI; 75 | private config: Config; 76 | 77 | /** 78 | * Create service manager instance 79 | * @param api API instance 80 | * @param config Configuration 81 | */ 82 | private constructor(api: MiniMaxAPI, config: Config) { 83 | this.api = api; 84 | this.config = config; 85 | } 86 | 87 | /** 88 | * Get service manager instance (singleton pattern) 89 | * @param api API instance 90 | * @param config Configuration 91 | * @returns Service manager instance 92 | */ 93 | public static getInstance(api: MiniMaxAPI, config: Config): ServiceManager { 94 | if (!ServiceManager.instance) { 95 | ServiceManager.instance = new ServiceManager(api, config); 96 | } 97 | return ServiceManager.instance; 98 | } 99 | 100 | /** 101 | * Register service 102 | * @param service Service instance 103 | */ 104 | public registerService(service: ServiceInterface): void { 105 | service.initialize(this.config); 106 | this.services.set(service.getServiceName(), service); 107 | } 108 | 109 | /** 110 | * Get service 111 | * @param serviceName Service name 112 | * @returns Service instance 113 | * @throws Error when service not found 114 | */ 115 | public getService(serviceName: string): T { 116 | const service = this.services.get(serviceName); 117 | if (!service) { 118 | throw new Error(`找不到服务: ${serviceName}`); 119 | } 120 | return service as T; 121 | } 122 | 123 | /** 124 | * Update configuration and API instance 125 | * @param config New configuration 126 | */ 127 | public updateConfig(config: Config): void { 128 | this.config = config; 129 | this.api = new MiniMaxAPI(config); 130 | 131 | // Update all services 132 | for (const service of this.services.values()) { 133 | if (service instanceof BaseService) { 134 | service.updateApi(this.api); 135 | } 136 | service.initialize(config); 137 | } 138 | } 139 | 140 | /** 141 | * Get all service names 142 | * @returns Array of service names 143 | */ 144 | public getServiceNames(): string[] { 145 | return [...this.services.keys()]; 146 | } 147 | } -------------------------------------------------------------------------------- /src/api/image.ts: -------------------------------------------------------------------------------- 1 | import { MiniMaxAPI } from '../utils/api.js'; 2 | import { ImageGenerationRequest } from '../types/index.js'; 3 | import { MinimaxRequestError } from '../exceptions/index.js'; 4 | import { DEFAULT_T2I_MODEL, ERROR_PROMPT_REQUIRED, RESOURCE_MODE_URL, VALID_IMAGE_MODELS } from '../const/index.js'; 5 | import * as path from 'path'; 6 | import * as fs from 'fs'; 7 | import { buildOutputFile } from '../utils/file.js'; 8 | import * as requests from 'axios'; 9 | 10 | export class ImageAPI { 11 | private api: MiniMaxAPI; 12 | 13 | constructor(api: MiniMaxAPI) { 14 | this.api = api; 15 | } 16 | 17 | async generateImage(request: ImageGenerationRequest): Promise { 18 | // Validate required parameters 19 | if (!request.prompt || request.prompt.trim() === '') { 20 | throw new MinimaxRequestError(ERROR_PROMPT_REQUIRED); 21 | } 22 | 23 | // Validate model 24 | const model = this.ensureValidModel(request.model); 25 | 26 | // Prepare request data 27 | const requestData: Record = { 28 | model: model, 29 | prompt: request.prompt, 30 | aspect_ratio: request.aspectRatio || '1:1', 31 | n: request.n || 1, 32 | prompt_optimizer: request.promptOptimizer !== undefined ? request.promptOptimizer : true 33 | }; 34 | 35 | // Only add subject reference if provided 36 | if (request.subjectReference) { 37 | // Check if it's a URL 38 | if (!request.subjectReference.startsWith(('http://')) && 39 | !request.subjectReference.startsWith(('https://')) && 40 | !request.subjectReference.startsWith(('data:'))) { 41 | // If it's a local file, process it as a data URL 42 | if (!fs.existsSync(request.subjectReference)) { 43 | throw new MinimaxRequestError(`Reference image file does not exist: ${request.subjectReference}`); 44 | } 45 | 46 | const imageData = fs.readFileSync(request.subjectReference); 47 | const base64Image = imageData.toString('base64'); 48 | requestData.subject_reference = `data:image/jpeg;base64,${base64Image}`; 49 | } else { 50 | requestData.subject_reference = request.subjectReference; 51 | } 52 | } 53 | 54 | // Send request 55 | const response = await this.api.post('/v1/image_generation', requestData); 56 | 57 | // Check response structure 58 | const imageUrls = response?.data?.image_urls; 59 | if (!imageUrls || !Array.isArray(imageUrls) || imageUrls.length === 0) { 60 | throw new MinimaxRequestError('Unable to get image URLs from response'); 61 | } 62 | 63 | // If URL mode, return URLs directly 64 | const resourceMode = this.api.getResourceMode(); 65 | if (resourceMode === RESOURCE_MODE_URL) { 66 | return imageUrls; 67 | } 68 | 69 | // Process output files 70 | const outputFiles: string[] = []; 71 | const outputDir = request.outputDirectory; 72 | 73 | for (let i = 0; i < imageUrls.length; i++) { 74 | // Generate output filename - similar to Python version 75 | const outputFileName = buildOutputFile(`image_${i}_${request.prompt.substring(0, 20)}`, outputDir, 'jpg', true); 76 | 77 | try { 78 | // Download image 79 | const imageResponse = await requests.default.get(imageUrls[i], { responseType: 'arraybuffer' }); 80 | 81 | // Ensure directory exists 82 | const dirPath = path.dirname(outputFileName); 83 | if (!fs.existsSync(dirPath)) { 84 | fs.mkdirSync(dirPath, { recursive: true }); 85 | } 86 | 87 | // Save file 88 | fs.writeFileSync(outputFileName, Buffer.from(imageResponse.data)); 89 | outputFiles.push(outputFileName); 90 | } catch (error) { 91 | throw new MinimaxRequestError(`Failed to download or save image: ${String(error)}`); 92 | } 93 | } 94 | 95 | return outputFiles; 96 | } 97 | 98 | // Helper function to validate model 99 | private ensureValidModel(model?: string): string { 100 | // Use default model if not provided 101 | if (!model) { 102 | return DEFAULT_T2I_MODEL; 103 | } 104 | 105 | // Validate if model is valid 106 | if (!VALID_IMAGE_MODELS.includes(model)) { 107 | // console.error(`Warning: Provided image model ${model} is invalid, using default value ${DEFAULT_T2I_MODEL}`); 108 | return DEFAULT_T2I_MODEL; 109 | } 110 | 111 | return model; 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/api/voice-clone.ts: -------------------------------------------------------------------------------- 1 | import { MiniMaxAPI } from '../utils/api.js'; 2 | import { VoiceCloneRequest } from '../types/index.js'; 3 | import { MinimaxRequestError } from '../exceptions/index.js'; 4 | import { DEFAULT_SPEECH_MODEL, ERROR_AUDIO_FILE_REQUIRED, RESOURCE_MODE_URL } from '../const/index.js'; 5 | import { buildOutputFile, processInputFile } from '../utils/file.js'; 6 | import * as fs from 'fs'; 7 | import * as path from 'path'; 8 | import * as requests from 'axios'; 9 | 10 | export class VoiceCloneAPI { 11 | private api: MiniMaxAPI; 12 | 13 | constructor(api: MiniMaxAPI) { 14 | this.api = api; 15 | } 16 | 17 | async cloneVoice(request: VoiceCloneRequest): Promise { 18 | // Validate required parameters 19 | if (!request.audioFile) { 20 | throw new MinimaxRequestError(ERROR_AUDIO_FILE_REQUIRED); 21 | } 22 | 23 | if (!request.voiceId) { 24 | throw new MinimaxRequestError('Voice ID is required'); 25 | } 26 | 27 | try { 28 | // Step 1: Upload file 29 | let files: any; 30 | 31 | if (request.isUrl) { 32 | // Handle URL file 33 | try { 34 | const response = await requests.default.get(request.audioFile, { responseType: 'stream' }); 35 | const tempFilePath = path.join(process.cwd(), 'temp', path.basename(request.audioFile)); 36 | 37 | // Ensure temp directory exists 38 | const tempDir = path.dirname(tempFilePath); 39 | if (!fs.existsSync(tempDir)) { 40 | fs.mkdirSync(tempDir, { recursive: true }); 41 | } 42 | 43 | // Save stream to temp file 44 | const writer = fs.createWriteStream(tempFilePath); 45 | response.data.pipe(writer); 46 | 47 | await new Promise((resolve, reject) => { 48 | writer.on('finish', () => resolve()); 49 | writer.on('error', reject); 50 | }); 51 | 52 | // Prepare upload parameters with temp file 53 | files = { 54 | file: { 55 | path: tempFilePath, 56 | }, 57 | }; 58 | } catch (error) { 59 | throw new MinimaxRequestError(`Failed to download audio from URL: ${String(error)}`); 60 | } 61 | } else { 62 | // Handle local file 63 | try { 64 | const filePath = processInputFile(request.audioFile); 65 | 66 | // Prepare upload parameters 67 | files = { 68 | file: { 69 | path: filePath, 70 | }, 71 | }; 72 | } catch (error) { 73 | throw new MinimaxRequestError(`Failed to read local file: ${String(error)}`); 74 | } 75 | } 76 | 77 | const data = { 78 | files, 79 | purpose: 'voice_clone', 80 | }; 81 | 82 | // Upload file 83 | const uploadResponse = await this.api.post('/v1/files/upload', data); 84 | 85 | // Get file ID 86 | const fileId = uploadResponse?.file?.file_id; 87 | if (!fileId) { 88 | throw new MinimaxRequestError('Failed to get file ID from upload response'); 89 | } 90 | 91 | // Step 2: Clone voice 92 | const payload: Record = { 93 | file_id: fileId, 94 | voice_id: request.voiceId, 95 | }; 96 | 97 | // If demo text is provided, add it to the request 98 | if (request.text) { 99 | payload.text = request.text; 100 | payload.model = DEFAULT_SPEECH_MODEL; 101 | } 102 | 103 | // Send clone request 104 | const cloneResponse = await this.api.post('/v1/voice_clone', payload); 105 | 106 | // Check if there's a demo audio 107 | const demoAudio = cloneResponse?.demo_audio; 108 | if (!demoAudio) { 109 | // If no demo audio, return voice ID directly 110 | return request.voiceId; 111 | } 112 | 113 | // If URL mode, return URL directly 114 | const resourceMode = this.api.getResourceMode(); 115 | if (resourceMode === RESOURCE_MODE_URL) { 116 | return demoAudio; 117 | } 118 | 119 | // Step 3: Download demo audio 120 | const outputPath = buildOutputFile('voice_clone', request.outputDirectory, 'wav', true); 121 | 122 | try { 123 | // Download audio 124 | const audioResponse = await requests.default.get(demoAudio, { responseType: 'arraybuffer' }); 125 | 126 | // Ensure directory exists 127 | const dirPath = path.dirname(outputPath); 128 | if (!fs.existsSync(dirPath)) { 129 | fs.mkdirSync(dirPath, { recursive: true }); 130 | } 131 | 132 | // Save file 133 | fs.writeFileSync(outputPath, Buffer.from(audioResponse.data)); 134 | 135 | // Return voice ID with path information 136 | return `${request.voiceId} (Demo audio: ${outputPath})`; 137 | } catch (error) { 138 | // If download fails, still return voice ID 139 | return request.voiceId; 140 | } 141 | } catch (error) { 142 | if (error instanceof MinimaxRequestError) { 143 | throw error; 144 | } 145 | throw new MinimaxRequestError(`Error occurred while cloning voice: ${String(error)}`); 146 | } 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /src/utils/api.ts: -------------------------------------------------------------------------------- 1 | import axios, { AxiosError, AxiosRequestConfig } from 'axios'; 2 | import { Config } from '../types/index.js'; 3 | import { createWriteStream } from 'fs'; 4 | import { 5 | MinimaxAuthError, 6 | MinimaxRequestError 7 | } from '../exceptions/index.js'; 8 | import * as fs from 'fs'; 9 | import * as path from 'path'; 10 | import { RESOURCE_MODE_URL } from '../const/index.js'; 11 | 12 | export class MiniMaxAPI { 13 | private config: Config; 14 | private baseURL: string; 15 | private session: ReturnType; 16 | 17 | constructor(config: Config) { 18 | this.config = config; 19 | this.baseURL = config.apiHost || 'https://api.minimax.chat'; 20 | this.session = axios.create({ 21 | headers: { 22 | 'Authorization': `Bearer ${this.config.apiKey}`, 23 | 'MM-API-Source': 'Minimax-MCP-JS' 24 | } 25 | }); 26 | } 27 | 28 | // Get current resource mode 29 | getResourceMode(): string { 30 | return this.config.resourceMode || RESOURCE_MODE_URL; 31 | } 32 | 33 | private getHeaders(hasFiles: boolean = false) { 34 | const headers: Record = { 35 | 'Authorization': `Bearer ${this.config.apiKey}`, 36 | }; 37 | 38 | // Set Content-Type based on whether files are being uploaded 39 | if (!hasFiles) { 40 | headers['Content-Type'] = 'application/json'; 41 | } 42 | 43 | return headers; 44 | } 45 | 46 | async makeRequest(endpoint: string, data: any, method: string = 'POST'): Promise { 47 | const url = `${this.baseURL}${endpoint}`; 48 | const hasFiles = !!data.files; 49 | const config: AxiosRequestConfig = { 50 | method, 51 | url, 52 | headers: this.getHeaders(hasFiles), 53 | }; 54 | 55 | if (method.toUpperCase() === 'GET') { 56 | config.params = data; 57 | } else { 58 | // Handle file uploads 59 | if (hasFiles) { 60 | // Use FormData for file uploads 61 | const formData = new FormData(); 62 | const files = data.files; 63 | delete data.files; 64 | 65 | // Add other data to FormData 66 | for (const [key, value] of Object.entries(data)) { 67 | if (value !== undefined) { 68 | formData.append(key, String(value)); 69 | } 70 | } 71 | 72 | // Add files 73 | for (const [fieldName, fileInfo] of Object.entries(files)) { 74 | const filePath = (fileInfo as any).path; 75 | if (filePath && fs.existsSync(filePath)) { 76 | const fileName = path.basename(filePath); 77 | const fileBuffer = fs.readFileSync(filePath); 78 | const fileBlob = new Blob([fileBuffer]); 79 | formData.append(fieldName, fileBlob, fileName); 80 | } 81 | } 82 | 83 | config.data = formData; 84 | } else { 85 | config.data = data; 86 | } 87 | } 88 | 89 | try { 90 | const response = await this.session.request(config); 91 | 92 | // Check for error codes in response data 93 | const baseResp = response.data?.base_resp; 94 | if (baseResp && baseResp.status_code !== 0) { 95 | // Throw different exceptions based on error code 96 | if (baseResp.status_code === 1004) { 97 | throw new MinimaxAuthError( 98 | `API Error: ${baseResp.status_msg}, Please check your API key and API host. ` + 99 | `Trace ID: ${response.headers['trace-id']}` 100 | ); 101 | } else { 102 | throw new MinimaxRequestError( 103 | `API Error: ${baseResp.status_msg} ` + 104 | `Trace ID: ${response.headers['trace-id']}` 105 | ); 106 | } 107 | } 108 | 109 | return response.data; 110 | } catch (error) { 111 | if (error instanceof MinimaxAuthError || error instanceof MinimaxRequestError) { 112 | throw error; 113 | } 114 | 115 | if (axios.isAxiosError(error)) { 116 | const axiosError = error as AxiosError; 117 | 118 | if (axiosError.response) { 119 | throw new MinimaxRequestError( 120 | `Request failed (${axiosError.response.status}): ${axiosError.response.statusText}. ` + 121 | `Response content: ${JSON.stringify(axiosError.response.data)}` 122 | ); 123 | } else if (axiosError.request) { 124 | throw new MinimaxRequestError( 125 | `Request sent but no response received, possibly a network issue: ${axiosError.message}` 126 | ); 127 | } else { 128 | throw new MinimaxRequestError(`Request error: ${axiosError.message}`); 129 | } 130 | } 131 | 132 | throw new MinimaxRequestError(`Unknown error: ${String(error)}`); 133 | } 134 | } 135 | 136 | async downloadFile(url: string, outputPath: string): Promise { 137 | try { 138 | // Ensure directory exists 139 | const dir = path.dirname(outputPath); 140 | if (!fs.existsSync(dir)) { 141 | fs.mkdirSync(dir, { recursive: true }); 142 | } 143 | 144 | const response = await axios({ 145 | method: 'GET', 146 | url, 147 | responseType: 'stream', 148 | headers: this.getHeaders(), 149 | }); 150 | 151 | const writer = createWriteStream(outputPath); 152 | response.data.pipe(writer); 153 | 154 | return new Promise((resolve, reject) => { 155 | writer.on('finish', resolve); 156 | writer.on('error', reject); 157 | }); 158 | } catch (error) { 159 | if (axios.isAxiosError(error)) { 160 | const axiosError = error as AxiosError; 161 | const errorMessage = axiosError.response?.data && 162 | typeof axiosError.response.data === 'object' && 163 | 'message' in axiosError.response.data 164 | ? (axiosError.response.data as any).message 165 | : axiosError.message; 166 | throw new MinimaxRequestError(`File download failed: ${errorMessage}`); 167 | } 168 | throw new MinimaxRequestError(`File download failed: ${String(error)}`); 169 | } 170 | } 171 | 172 | async get(endpoint: string, params: any = {}): Promise { 173 | return this.makeRequest(endpoint, params, 'GET'); 174 | } 175 | 176 | async post(endpoint: string, data: any = {}): Promise { 177 | return this.makeRequest(endpoint, data, 'POST'); 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /src/api/music.ts: -------------------------------------------------------------------------------- 1 | import { MiniMaxAPI } from '../utils/api.js'; 2 | import { MinimaxRequestError } from '../exceptions/index.js'; 3 | import { MusicGenerationRequest } from '../types/index.js'; 4 | import { DEFAULT_FORMAT, DEFAULT_MUSIC_MODEL, ERROR_LYRICS_REQUIRED, ERROR_PROMPT_REQUIRED, RESOURCE_MODE_URL } from '../const/index.js'; 5 | import * as path from 'path'; 6 | import { buildOutputFile } from '../utils/file.js'; 7 | import * as fs from 'fs'; 8 | 9 | export class MusicAPI { 10 | private api: MiniMaxAPI; 11 | 12 | constructor(api: MiniMaxAPI) { 13 | this.api = api; 14 | } 15 | 16 | async generateMusic(request: MusicGenerationRequest): Promise { 17 | // Validate required parameters 18 | if (!request.prompt || request.prompt.trim() === '') { 19 | throw new MinimaxRequestError(ERROR_PROMPT_REQUIRED); 20 | } 21 | if (!request.lyrics || request.lyrics.trim() === '') { 22 | throw new MinimaxRequestError(ERROR_LYRICS_REQUIRED); 23 | } 24 | 25 | // Process output file 26 | const textPrefix = request.prompt.substring(0, 20).replace(/[^\w]/g, '_'); 27 | const format = request.format || DEFAULT_FORMAT; 28 | const fileName = `music_${textPrefix}_${Date.now()}`; 29 | const outputFile = buildOutputFile(fileName, request.outputDirectory, format); 30 | 31 | // Prepare request data 32 | const requestData: Record = { 33 | model: DEFAULT_MUSIC_MODEL, 34 | prompt: request.prompt, 35 | lyrics: request.lyrics, 36 | audio_setting: { 37 | sample_rate: this.ensureValidSampleRate(request.sampleRate), 38 | bitrate: this.ensureValidBitrate(request.bitrate), 39 | format: this.ensureValidFormat(request.format), 40 | channel: this.ensureValidChannel(request.channel), 41 | }, 42 | }; 43 | 44 | // Add output format (if specified) 45 | if (request.outputFormat === RESOURCE_MODE_URL) { 46 | requestData.output_format = 'url'; 47 | } 48 | 49 | try { 50 | // Send request 51 | const response = await this.api.post('/v1/music_generation', requestData); 52 | 53 | // Process response 54 | const audioData = response?.data?.audio; 55 | 56 | if (!audioData) { 57 | throw new MinimaxRequestError('Could not get audio data from response'); 58 | } 59 | 60 | // If URL mode, return URL directly 61 | if (request.outputFormat === RESOURCE_MODE_URL) { 62 | return audioData; 63 | } 64 | 65 | // decode and save file 66 | try { 67 | // Convert hex string to binary 68 | const audioBuffer = Buffer.from(audioData, 'hex'); 69 | 70 | // Ensure output directory exists 71 | const outputDir = path.dirname(outputFile); 72 | if (!fs.existsSync(outputDir)) { 73 | fs.mkdirSync(outputDir, { recursive: true }); 74 | } 75 | 76 | // Write to file 77 | fs.writeFileSync(outputFile, audioBuffer); 78 | 79 | return outputFile; 80 | } catch (error) { 81 | throw new MinimaxRequestError(`Failed to save audio file: ${String(error)}`); 82 | } 83 | } catch (err) { 84 | throw err; 85 | } 86 | } 87 | 88 | // Helper function: Ensure sample rate is within valid range 89 | private ensureValidSampleRate(sampleRate?: number): number { 90 | // List of valid sample rates supported by MiniMax API 91 | const validSampleRates = [16000, 24000, 32000, 44100]; 92 | 93 | // If no sample rate is provided or it's invalid, use default value 32000 94 | if (sampleRate === undefined) { 95 | return 32000; 96 | } 97 | 98 | // If the provided sample rate is not within the valid range, use the closest valid value 99 | if (!validSampleRates.includes(sampleRate)) { 100 | // Find the closest valid sample rate 101 | const closest = validSampleRates.reduce((prev, curr) => { 102 | return Math.abs(curr - sampleRate) < Math.abs(prev - sampleRate) ? curr : prev; 103 | }); 104 | 105 | console.error(`Warning: Provided sample rate ${sampleRate} is invalid, using closest valid value ${closest}`); 106 | return closest; 107 | } 108 | 109 | return sampleRate; 110 | } 111 | 112 | // Helper function: Ensure bitrate is within valid range 113 | private ensureValidBitrate(bitrate?: number): number { 114 | // List of valid bitrates supported by MiniMax API 115 | const validBitrates = [32000, 64000, 128000, 256000]; 116 | 117 | // If no bitrate is provided or it's invalid, use default value 128000 118 | if (bitrate === undefined) { 119 | return 128000; 120 | } 121 | 122 | // If the provided bitrate is not within the valid range, use the closest valid value 123 | if (!validBitrates.includes(bitrate)) { 124 | // Find the closest valid bitrate 125 | const closest = validBitrates.reduce((prev, curr) => { 126 | return Math.abs(curr - bitrate) < Math.abs(prev - bitrate) ? curr : prev; 127 | }); 128 | 129 | console.error(`Warning: Provided bitrate ${bitrate} is invalid, using closest valid value ${closest}`); 130 | return closest; 131 | } 132 | 133 | return bitrate; 134 | } 135 | 136 | // Helper function: Ensure channel is within valid range 137 | private ensureValidChannel(channel?: number): number { 138 | // List of valid channels supported by MiniMax API 139 | const validChannels = [1, 2]; 140 | 141 | // If no channel is provided or it's invalid, use default value 1 142 | if (channel === undefined) { 143 | return 1; 144 | } 145 | 146 | // If the provided channel is not within the valid range, use the closest valid value 147 | if (!validChannels.includes(channel)) { 148 | // Find the closest valid channel 149 | const closest = validChannels.reduce((prev, curr) => { 150 | return Math.abs(curr - channel) < Math.abs(prev - channel) ? curr : prev; 151 | }); 152 | 153 | console.error(`Warning: Provided channel ${channel} is invalid, using closest valid value ${closest}`); 154 | return closest; 155 | } 156 | 157 | return channel; 158 | } 159 | 160 | // Helper function: Ensure format is within valid range 161 | private ensureValidFormat(format?: string): string { 162 | // List of valid formats supported by MiniMax API 163 | const validFormats = ['mp3', 'pcm', 'wav']; 164 | 165 | // If no format is provided or it's invalid, use default value mp3 166 | if (!format) { 167 | return 'mp3'; 168 | } 169 | 170 | // If the provided format is not within the valid range, use default value 171 | if (!validFormats.includes(format)) { 172 | console.error(`Warning: Provided format ${format} is invalid, using default value mp3`); 173 | return 'mp3'; 174 | } 175 | 176 | return format; 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /src/api/video.ts: -------------------------------------------------------------------------------- 1 | import { MiniMaxAPI } from '../utils/api.js'; 2 | import { VideoGenerationQueryRequest, VideoGenerationRequest } from '../types/index.js'; 3 | import { MinimaxRequestError } from '../exceptions/index.js'; 4 | import { DEFAULT_VIDEO_MODEL, ERROR_PROMPT_REQUIRED, RESOURCE_MODE_URL, VALID_VIDEO_MODELS } from '../const/index.js'; 5 | import * as path from 'path'; 6 | import * as fs from 'fs'; 7 | import { buildOutputFile } from '../utils/file.js'; 8 | import * as requests from 'axios'; 9 | 10 | export class VideoAPI { 11 | private api: MiniMaxAPI; 12 | 13 | constructor(api: MiniMaxAPI) { 14 | this.api = api; 15 | } 16 | 17 | async generateVideo(request: VideoGenerationRequest): Promise { 18 | // Validate required parameters 19 | if (!request.prompt || request.prompt.trim() === '') { 20 | throw new MinimaxRequestError(ERROR_PROMPT_REQUIRED); 21 | } 22 | 23 | try { 24 | // Ensure model is valid 25 | const model = this.ensureValidModel(request.model); 26 | 27 | // Prepare request data 28 | const requestData: Record = { 29 | model: model, 30 | prompt: request.prompt 31 | }; 32 | 33 | // Process first frame image 34 | if (request.firstFrameImage) { 35 | // Check if it's a URL or data URL 36 | if (!request.firstFrameImage.startsWith(('http://')) && 37 | !request.firstFrameImage.startsWith(('https://')) && 38 | !request.firstFrameImage.startsWith(('data:'))) { 39 | // If it's a local file, convert to data URL 40 | if (!fs.existsSync(request.firstFrameImage)) { 41 | throw new MinimaxRequestError(`First frame image file does not exist: ${request.firstFrameImage}`); 42 | } 43 | 44 | const imageData = fs.readFileSync(request.firstFrameImage); 45 | const base64Image = imageData.toString('base64'); 46 | requestData.first_frame_image = `data:image/jpeg;base64,${base64Image}`; 47 | } else { 48 | requestData.first_frame_image = request.firstFrameImage; 49 | } 50 | } 51 | 52 | // Process resolution 53 | if (request.resolution) { 54 | requestData.resolution = request.resolution; 55 | } 56 | 57 | // Process duration 58 | if (request.duration) { 59 | requestData.duration = request.duration; 60 | } 61 | 62 | // Step 1: Submit video generation task 63 | const response = await this.api.post('/v1/video_generation', requestData); 64 | 65 | // Get task ID 66 | const taskId = response?.task_id; 67 | if (!taskId) { 68 | throw new MinimaxRequestError('Unable to get task ID from response'); 69 | } 70 | 71 | if (request.asyncMode) { 72 | return { 73 | task_id: taskId, 74 | } 75 | } 76 | 77 | // Step 2: Wait for video generation task to complete 78 | let fileId: string | null = null; 79 | const maxRetries = model === "MiniMax-Hailuo-02" ? 60 : 30; // Maximum 30 attempts, total duration 10 minutes (30 * 20 seconds). MiniMax-Hailuo-02 model has a longer processing time, so we need to wait for a longer time 80 | const retryInterval = 20; // 20 second interval 81 | 82 | for (let attempt = 0; attempt < maxRetries; attempt++) { 83 | // Query task status 84 | const statusResponse = await this.api.get(`/v1/query/video_generation?task_id=${taskId}`); 85 | const status = statusResponse?.status; 86 | 87 | if (status === 'Fail') { 88 | throw new MinimaxRequestError(`Video generation task failed, task ID: ${taskId}`); 89 | } else if (status === 'Success') { 90 | fileId = statusResponse?.file_id; 91 | if (fileId) { 92 | break; 93 | } 94 | throw new MinimaxRequestError(`File ID missing in success response, task ID: ${taskId}`); 95 | } 96 | 97 | // Task still processing, wait and retry 98 | await new Promise(resolve => setTimeout(resolve, retryInterval * 1000)); 99 | } 100 | 101 | if (!fileId) { 102 | throw new MinimaxRequestError(`Failed to get file ID, task ID: ${taskId}`); 103 | } 104 | 105 | // Step 3: Get video result 106 | const fileResponse = await this.api.get(`/v1/files/retrieve?file_id=${fileId}`); 107 | const downloadUrl = fileResponse?.file?.download_url; 108 | 109 | if (!downloadUrl) { 110 | throw new MinimaxRequestError(`Unable to get download URL for file ID: ${fileId}`); 111 | } 112 | 113 | // If URL mode, return URL directly 114 | const resourceMode = this.api.getResourceMode(); 115 | if (resourceMode === RESOURCE_MODE_URL) { 116 | return { 117 | video_url: downloadUrl, 118 | task_id: taskId, 119 | }; 120 | } 121 | 122 | // Step 4: Download and save video 123 | const outputPath = buildOutputFile(`video_${taskId}`, request.outputDirectory, 'mp4', true); 124 | 125 | try { 126 | const videoResponse = await requests.default.get(downloadUrl, { responseType: 'arraybuffer' }); 127 | 128 | // Ensure directory exists 129 | const dirPath = path.dirname(outputPath); 130 | if (!fs.existsSync(dirPath)) { 131 | fs.mkdirSync(dirPath, { recursive: true }); 132 | } 133 | 134 | // Save file 135 | fs.writeFileSync(outputPath, Buffer.from(videoResponse.data)); 136 | return { 137 | video_path: outputPath, 138 | task_id: taskId, 139 | } 140 | } catch (error) { 141 | throw new MinimaxRequestError(`Failed to download or save video: ${String(error)}`); 142 | } 143 | } catch (error) { 144 | if (error instanceof MinimaxRequestError) { 145 | throw error; 146 | } 147 | throw new MinimaxRequestError(`Unexpected error occurred during video generation: ${String(error)}`); 148 | } 149 | } 150 | 151 | async queryVideoGeneration(request: VideoGenerationQueryRequest): Promise { 152 | const taskId = request.taskId; 153 | 154 | // Step 1: Get video generation status 155 | const response = await this.api.get(`/v1/query/video_generation?task_id=${taskId}`); 156 | const status = response?.status; 157 | let fileId: string | null = null; 158 | if (status === 'Fail') { 159 | throw new MinimaxRequestError(`Video generation task failed, task ID: ${taskId}`); 160 | } else if (status === 'Success') { 161 | fileId = response?.file_id; 162 | if (!fileId) { 163 | throw new MinimaxRequestError(`File ID missing in success response, task ID: ${taskId}`); 164 | } 165 | } else { 166 | return { 167 | status, 168 | } 169 | } 170 | 171 | // Step 2: Get video result 172 | const fileResponse = await this.api.get(`/v1/files/retrieve?file_id=${fileId}`); 173 | const downloadUrl = fileResponse?.file?.download_url; 174 | 175 | if (!downloadUrl) { 176 | throw new MinimaxRequestError(`Unable to get download URL for file ID: ${fileId}`); 177 | } 178 | 179 | // If URL mode, return URL directly 180 | const resourceMode = this.api.getResourceMode(); 181 | if (resourceMode === RESOURCE_MODE_URL) { 182 | return { 183 | status, 184 | video_url: downloadUrl, 185 | task_id: taskId, 186 | }; 187 | } 188 | 189 | // Step 3: Download and save video 190 | const outputPath = buildOutputFile(`video_${taskId}`, request.outputDirectory, 'mp4', true); 191 | 192 | try { 193 | const videoResponse = await requests.default.get(downloadUrl, { responseType: 'arraybuffer' }); 194 | 195 | // Ensure directory exists 196 | const dirPath = path.dirname(outputPath); 197 | if (!fs.existsSync(dirPath)) { 198 | fs.mkdirSync(dirPath, { recursive: true }); 199 | } 200 | 201 | // Save file 202 | fs.writeFileSync(outputPath, Buffer.from(videoResponse.data)); 203 | return { 204 | status, 205 | video_path: outputPath, 206 | task_id: taskId, 207 | } 208 | } catch (error) { 209 | throw new MinimaxRequestError(`Failed to download or save video: ${String(error)}`); 210 | } 211 | } 212 | 213 | // Helper function: Ensure model is valid 214 | private ensureValidModel(model?: string): string { 215 | // If no model provided, use default 216 | if (!model) { 217 | return DEFAULT_VIDEO_MODEL; 218 | } 219 | 220 | // Check if model is valid 221 | if (!VALID_VIDEO_MODELS.includes(model)) { 222 | // console.error(`Warning: Provided model ${model} is invalid, using default value ${DEFAULT_VIDEO_MODEL}`); 223 | return DEFAULT_VIDEO_MODEL; 224 | } 225 | 226 | return model; 227 | } 228 | } 229 | -------------------------------------------------------------------------------- /src/services/media-service.ts: -------------------------------------------------------------------------------- 1 | import { BaseService } from './index.js'; 2 | import { MiniMaxAPI } from '../utils/api.js'; 3 | import { TTSAPI } from '../api/tts.js'; 4 | import { ImageAPI } from '../api/image.js'; 5 | import { VideoAPI } from '../api/video.js'; 6 | import { VoiceCloneAPI } from '../api/voice-clone.js'; 7 | import { VoiceAPI } from '../api/voice.js'; 8 | import { MusicAPI } from '../api/music.js'; 9 | import { VoiceDesignAPI } from '../api/voice-design.js'; 10 | import { Config } from '../types/index.js'; 11 | import { RESOURCE_MODE_URL } from '../const/index.js'; 12 | 13 | /** 14 | * Media service, handles all media-related operations 15 | */ 16 | export class MediaService extends BaseService { 17 | private ttsApi: TTSAPI; 18 | private imageApi: ImageAPI; 19 | private videoApi: VideoAPI; 20 | private voiceCloneApi: VoiceCloneAPI; 21 | private voiceApi: VoiceAPI; 22 | private musicApi: MusicAPI; 23 | private voiceDesignApi: VoiceDesignAPI; 24 | 25 | /** 26 | * Create media service instance 27 | * @param api API instance 28 | */ 29 | constructor(api: MiniMaxAPI) { 30 | super(api, 'media-service'); 31 | this.ttsApi = new TTSAPI(api); 32 | this.imageApi = new ImageAPI(api); 33 | this.videoApi = new VideoAPI(api); 34 | this.voiceCloneApi = new VoiceCloneAPI(api); 35 | this.voiceApi = new VoiceAPI(api); 36 | this.musicApi = new MusicAPI(api); 37 | this.voiceDesignApi = new VoiceDesignAPI(api); 38 | this.config = {} as Config; // Initialize as empty object, will be set in initialize 39 | } 40 | 41 | /** 42 | * Initialize service 43 | * @param config Configuration 44 | */ 45 | public async initialize(config: Config): Promise { 46 | await super.initialize(config); 47 | // Any media service specific initialization logic 48 | } 49 | 50 | /** 51 | * Update API instance 52 | * @param api New API instance 53 | */ 54 | public updateApi(api: MiniMaxAPI): void { 55 | super.updateApi(api); 56 | this.ttsApi = new TTSAPI(api); 57 | this.imageApi = new ImageAPI(api); 58 | this.videoApi = new VideoAPI(api); 59 | this.voiceCloneApi = new VoiceCloneAPI(api); 60 | this.voiceApi = new VoiceAPI(api); 61 | this.musicApi = new MusicAPI(api); 62 | this.voiceDesignApi = new VoiceDesignAPI(api); 63 | } 64 | 65 | /** 66 | * Generate speech 67 | * @param params Speech generation parameters 68 | * @returns Generation result (URL or file path) 69 | */ 70 | public async generateSpeech(params: any): Promise { 71 | this.checkInitialized(); 72 | try { 73 | return await this.ttsApi.generateSpeech(params); 74 | } catch (error) { 75 | // console.error(`[${new Date().toISOString()}] Failed to generate speech:`, error); 76 | throw this.wrapError('Failed to generate speech', error); 77 | } 78 | } 79 | 80 | /** 81 | * List available voices 82 | * @param params Voice listing parameters 83 | * @returns Voice list results 84 | */ 85 | public async listVoices(params: any): Promise<{systemVoices: string[], voiceCloneVoices: string[]}> { 86 | this.checkInitialized(); 87 | try { 88 | return await this.voiceApi.listVoices(params); 89 | } catch (error) { 90 | // console.error(`[${new Date().toISOString()}] Failed to get voice list:`, error); 91 | throw this.wrapError('Failed to get voice list', error); 92 | } 93 | } 94 | 95 | /** 96 | * Clone voice 97 | * @param params Voice cloning parameters 98 | * @returns Cloning result 99 | */ 100 | public async cloneVoice(params: any): Promise { 101 | this.checkInitialized(); 102 | try { 103 | return await this.voiceCloneApi.cloneVoice(params); 104 | } catch (error) { 105 | // console.error(`[${new Date().toISOString()}] Failed to clone voice:`, error); 106 | throw this.wrapError('Failed to clone voice', error); 107 | } 108 | } 109 | 110 | /** 111 | * Generate image 112 | * @param params Image generation parameters 113 | * @returns Generation results (URL array or file path array) 114 | */ 115 | public async generateImage(params: any): Promise { 116 | this.checkInitialized(); 117 | try { 118 | // Auto-generate output filename if not provided 119 | if (!params.outputFile) { 120 | const promptPrefix = params.prompt.substring(0, 20).replace(/[^\w]/g, '_'); 121 | params.outputFile = `image_${promptPrefix}_${Date.now()}`; 122 | } 123 | 124 | const outputFiles = await this.imageApi.generateImage(params); 125 | return outputFiles; 126 | } catch (error) { 127 | // console.error(`[${new Date().toISOString()}] Failed to generate image:`, error); 128 | throw this.wrapError('Failed to generate image', error); 129 | } 130 | } 131 | 132 | /** 133 | * Generate video 134 | * @param params Video generation parameters 135 | * @returns Generation result (URL or file path) 136 | */ 137 | public async generateVideo(params: any): Promise { 138 | this.checkInitialized(); 139 | try { 140 | // Auto-generate output filename if not provided 141 | if (!params.outputFile) { 142 | const promptPrefix = params.prompt.substring(0, 20).replace(/[^\w]/g, '_'); 143 | params.outputFile = `video_${promptPrefix}_${Date.now()}`; 144 | } 145 | 146 | const result = await this.videoApi.generateVideo(params); 147 | if (params.async_mode) { 148 | return { 149 | content: [ 150 | { 151 | type: 'text', 152 | 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.`, 153 | }, 154 | ], 155 | }; 156 | } else if (this.config.resourceMode === RESOURCE_MODE_URL) { 157 | return { 158 | content: [ 159 | { 160 | type: 'text', 161 | text: `Success. Video URL: ${result.video_url}`, 162 | }, 163 | ], 164 | }; 165 | } else { 166 | return { 167 | content: [ 168 | { 169 | type: 'text', 170 | text: `Success. Video saved as: ${result.video_path}`, 171 | }, 172 | ], 173 | }; 174 | } 175 | } catch (error) { 176 | // console.error(`[${new Date().toISOString()}] Failed to generate video:`, error); 177 | throw this.wrapError('Failed to generate video', error); 178 | } 179 | } 180 | 181 | /** 182 | * Query video generation 183 | * @param params Video generation query parameters 184 | * @returns Query result 185 | */ 186 | public async queryVideoGeneration(params: any): Promise { 187 | this.checkInitialized(); 188 | try { 189 | const result = await this.videoApi.queryVideoGeneration(params); 190 | if (result.status === 'Success') { 191 | if (this.config.resourceMode === RESOURCE_MODE_URL) { 192 | return { 193 | content: [ 194 | { 195 | type: 'text', 196 | text: `Success. Video URL: ${result.video_url}`, 197 | }, 198 | ], 199 | }; 200 | } else { 201 | return { 202 | content: [ 203 | { 204 | type: 'text', 205 | text: `Success. Video saved as: ${result.video_path}`, 206 | }, 207 | ], 208 | }; 209 | } 210 | } else { 211 | return { 212 | content: [ 213 | { 214 | type: 'text', 215 | text: `Video generation task is still processing: Task ID: ${params.taskId}.`, 216 | }, 217 | ], 218 | }; 219 | } 220 | } catch (error) { 221 | throw this.wrapError('Failed to query video generation status', error); 222 | } 223 | } 224 | 225 | /** 226 | * Generate music 227 | * @param params Music generation parameters 228 | * @returns Generation result (file path) 229 | */ 230 | public async generateMusic(params: any): Promise { 231 | this.checkInitialized(); 232 | try { 233 | return await this.musicApi.generateMusic(params); 234 | } catch (error) { 235 | throw this.wrapError('Failed to generate music', error); 236 | } 237 | } 238 | 239 | /** 240 | * Design voice 241 | * @param params Voice design parameters 242 | * @returns Design result (voice ID and file path) 243 | */ 244 | public async designVoice(params: any): Promise { 245 | this.checkInitialized(); 246 | try { 247 | return await this.voiceDesignApi.voiceDesign(params); 248 | } catch (error) { 249 | throw this.wrapError('Failed to design voice', error); 250 | } 251 | } 252 | 253 | /** 254 | * Wrap error message 255 | * @param message Error message prefix 256 | * @param error Original error 257 | * @returns Wrapped error 258 | */ 259 | private wrapError(message: string, error: unknown): Error { 260 | if (error instanceof Error) { 261 | const wrappedError = new Error(`${message}: ${error.message}`); 262 | wrappedError.stack = error.stack; 263 | return wrappedError; 264 | } 265 | return new Error(`${message}: ${String(error)}`); 266 | } 267 | } 268 | -------------------------------------------------------------------------------- /src/api/tts.ts: -------------------------------------------------------------------------------- 1 | import { MiniMaxAPI } from '../utils/api.js'; 2 | import { TTSRequest } from '../types/index.js'; 3 | import { MinimaxRequestError } from '../exceptions/index.js'; 4 | import { ERROR_TEXT_REQUIRED, RESOURCE_MODE_URL } from '../const/index.js'; 5 | import * as path from 'path'; 6 | import { buildOutputFile } from '../utils/file.js'; 7 | import * as fs from 'fs'; 8 | 9 | export class TTSAPI { 10 | private api: MiniMaxAPI; 11 | 12 | constructor(api: MiniMaxAPI) { 13 | this.api = api; 14 | } 15 | 16 | async generateSpeech(request: TTSRequest): Promise { 17 | // Validate required parameters 18 | if (!request.text || request.text.trim() === '') { 19 | throw new MinimaxRequestError(ERROR_TEXT_REQUIRED); 20 | } 21 | 22 | // Process output file 23 | let outputFile = request.outputFile; 24 | if (!outputFile) { 25 | // If no output file is provided, generate one based on text content 26 | const textPrefix = request.text.substring(0, 20).replace(/[^\w]/g, '_'); 27 | outputFile = `tts_${textPrefix}_${Date.now()}`; 28 | } 29 | 30 | if (!path.extname(outputFile)) { 31 | // If no extension, add one based on format 32 | const format = request.format || 'mp3'; 33 | outputFile = buildOutputFile(outputFile, request.outputDirectory, format); 34 | } 35 | 36 | // Prepare request data according to MiniMax API nested structure 37 | const requestData: Record = { 38 | model: this.ensureValidModel(request.model), 39 | text: request.text, 40 | voice_setting: { 41 | voice_id: request.voiceId || 'male-qn-qingse', 42 | speed: request.speed || 1.0, 43 | vol: request.vol || 1.0, 44 | pitch: request.pitch || 0, 45 | emotion: this.ensureValidEmotion(request.emotion, this.ensureValidModel(request.model)) 46 | }, 47 | audio_setting: { 48 | sample_rate: this.ensureValidSampleRate(request.sampleRate), 49 | bitrate: this.ensureValidBitrate(request.bitrate), 50 | format: this.ensureValidFormat(request.format), 51 | channel: this.ensureValidChannel(request.channel) 52 | }, 53 | language_boost: request.languageBoost || 'auto', 54 | stream: request.stream, 55 | subtitle_enable: request.subtitleEnable 56 | }; 57 | 58 | // Add output format (if specified) 59 | if (request.outputFormat === RESOURCE_MODE_URL) { 60 | requestData.output_format = 'url'; 61 | } 62 | 63 | // Filter out undefined fields (recursive) 64 | const filteredData = this.removeUndefinedFields(requestData); 65 | 66 | try { 67 | // Send request 68 | const response = await this.api.post('/v1/t2a_v2', filteredData); 69 | 70 | // Process response 71 | const audioData = response?.data?.audio; 72 | 73 | const subtitleFile = response?.data?.subtitle_file; 74 | 75 | if (!audioData) { 76 | throw new MinimaxRequestError('Could not get audio data from response'); 77 | } 78 | 79 | // If URL mode, return URL directly 80 | if (request.outputFormat === RESOURCE_MODE_URL) { 81 | return { 82 | audio: audioData, 83 | subtitle: subtitleFile 84 | }; 85 | } 86 | 87 | // If base64 mode, decode and save file 88 | try { 89 | // Convert hex string to binary 90 | const audioBuffer = Buffer.from(audioData, 'hex'); 91 | 92 | // Ensure output directory exists 93 | const outputDir = path.dirname(outputFile); 94 | if (!fs.existsSync(outputDir)) { 95 | fs.mkdirSync(outputDir, { recursive: true }); 96 | } 97 | 98 | // Write to file 99 | fs.writeFileSync(outputFile, audioBuffer); 100 | 101 | return { 102 | audio: outputFile, 103 | subtitle: subtitleFile 104 | }; 105 | } catch (error) { 106 | throw new MinimaxRequestError(`Failed to save audio file: ${String(error)}`); 107 | } 108 | } catch (error) { 109 | throw error; 110 | } 111 | } 112 | 113 | // Helper function: Recursively remove undefined fields from an object 114 | private removeUndefinedFields(obj: any): any { 115 | if (typeof obj !== 'object' || obj === null) { 116 | return obj; 117 | } 118 | 119 | if (Array.isArray(obj)) { 120 | return obj.map(item => this.removeUndefinedFields(item)).filter(item => item !== undefined); 121 | } 122 | 123 | const result: Record = {}; 124 | for (const [key, value] of Object.entries(obj)) { 125 | if (value === undefined) continue; 126 | 127 | if (typeof value === 'object' && value !== null) { 128 | const filteredValue = this.removeUndefinedFields(value); 129 | // Only add non-empty objects 130 | if (typeof filteredValue === 'object' && !Array.isArray(filteredValue) && Object.keys(filteredValue).length === 0) { 131 | continue; 132 | } 133 | result[key] = filteredValue; 134 | } else { 135 | result[key] = value; 136 | } 137 | } 138 | 139 | return result; 140 | } 141 | 142 | // Helper function: Ensure sample rate is within valid range 143 | private ensureValidSampleRate(sampleRate?: number): number { 144 | // List of valid sample rates supported by MiniMax API 145 | const validSampleRates = [8000, 16000, 22050, 24000, 32000, 44100]; 146 | 147 | // If no sample rate is provided or it's invalid, use default value 32000 148 | if (sampleRate === undefined) { 149 | return 32000; 150 | } 151 | 152 | // If the provided sample rate is not within the valid range, use the closest valid value 153 | if (!validSampleRates.includes(sampleRate)) { 154 | // Find the closest valid sample rate 155 | const closest = validSampleRates.reduce((prev, curr) => { 156 | return (Math.abs(curr - sampleRate) < Math.abs(prev - sampleRate)) ? curr : prev; 157 | }); 158 | 159 | // console.error(`Warning: Provided sample rate ${sampleRate} is invalid, using closest valid value ${closest}`); 160 | return closest; 161 | } 162 | 163 | return sampleRate; 164 | } 165 | 166 | // Helper function: Ensure bitrate is within valid range 167 | private ensureValidBitrate(bitrate?: number): number { 168 | // List of valid bitrates supported by MiniMax API 169 | const validBitrates = [64000, 96000, 128000, 160000, 192000, 224000, 256000, 320000]; 170 | 171 | // If no bitrate is provided or it's invalid, use default value 128000 172 | if (bitrate === undefined) { 173 | return 128000; 174 | } 175 | 176 | // If the provided bitrate is not within the valid range, use the closest valid value 177 | if (!validBitrates.includes(bitrate)) { 178 | // Find the closest valid bitrate 179 | const closest = validBitrates.reduce((prev, curr) => { 180 | return (Math.abs(curr - bitrate) < Math.abs(prev - bitrate)) ? curr : prev; 181 | }); 182 | 183 | // console.error(`Warning: Provided bitrate ${bitrate} is invalid, using closest valid value ${closest}`); 184 | return closest; 185 | } 186 | 187 | return bitrate; 188 | } 189 | 190 | // Helper function: Ensure channel is within valid range 191 | private ensureValidChannel(channel?: number): number { 192 | // List of valid channels supported by MiniMax API 193 | const validChannels = [1, 2]; 194 | 195 | // If no channel is provided or it's invalid, use default value 1 196 | if (channel === undefined) { 197 | return 1; 198 | } 199 | 200 | // If the provided channel is not within the valid range, use the closest valid value 201 | if (!validChannels.includes(channel)) { 202 | // Find the closest valid channel 203 | const closest = validChannels.reduce((prev, curr) => { 204 | return (Math.abs(curr - channel) < Math.abs(prev - channel)) ? curr : prev; 205 | }); 206 | 207 | // console.error(`Warning: Provided channel ${channel} is invalid, using closest valid value ${closest}`); 208 | return closest; 209 | } 210 | 211 | return channel; 212 | } 213 | 214 | // Helper function: Ensure model is within valid range 215 | private ensureValidModel(model?: string): string { 216 | // List of valid models supported by MiniMax API 217 | const validModels = ['speech-02-hd', 'speech-02-turbo', 'speech-01-hd', 'speech-01-turbo', 'speech-01-240228', 'speech-01-turbo-240228']; 218 | 219 | // If no model is provided or it's invalid, use default value speech-02-hd 220 | if (!model) { 221 | return 'speech-02-hd'; 222 | } 223 | 224 | // If the provided model is not within the valid range, use default value 225 | if (!validModels.includes(model)) { 226 | // console.error(`Warning: Provided model ${model} is invalid, using default value speech-02-hd`); 227 | return 'speech-02-hd'; 228 | } 229 | 230 | return model; 231 | } 232 | 233 | // Helper function: Ensure format is within valid range 234 | private ensureValidFormat(format?: string): string { 235 | // List of valid formats supported by MiniMax API 236 | const validFormats = ['mp3', 'pcm', 'flac', 'wav']; 237 | 238 | // If no format is provided or it's invalid, use default value mp3 239 | if (!format) { 240 | return 'mp3'; 241 | } 242 | 243 | // If the provided format is not within the valid range, use default value 244 | if (!validFormats.includes(format)) { 245 | // console.error(`Warning: Provided format ${format} is invalid, using default value mp3`); 246 | return 'mp3'; 247 | } 248 | 249 | return format; 250 | } 251 | 252 | // Helper function: Ensure emotion is within valid range and compatible with the model 253 | private ensureValidEmotion(emotion?: string, model?: string): string | undefined { 254 | // List of valid emotions supported by MiniMax API 255 | const validEmotions = ['happy', 'sad', 'angry', 'fearful', 'disgusted', 'surprised', 'neutral']; 256 | 257 | // List of models that support emotion parameter 258 | const emotionSupportedModels = ['speech-02-hd', 'speech-02-turbo', 'speech-01-turbo', 'speech-01-hd']; 259 | 260 | // Check if the model supports emotion 261 | if (model && !emotionSupportedModels.includes(model)) { 262 | return undefined; // Return undefined to remove the emotion parameter for unsupported models 263 | } 264 | 265 | // If no emotion is provided or it's invalid, use default value happy 266 | if (!emotion) { 267 | return 'happy'; 268 | } 269 | 270 | // If the provided emotion is not within the valid range, use default value 271 | if (!validEmotions.includes(emotion)) { 272 | // console.error(`Warning: Provided emotion ${emotion} is invalid, using default value happy`); 273 | return 'happy'; 274 | } 275 | 276 | return emotion; 277 | } 278 | } 279 | -------------------------------------------------------------------------------- /src/config/ConfigManager.ts: -------------------------------------------------------------------------------- 1 | import { Config, TransportMode } from '../types/index.js'; 2 | import fs from 'fs'; 3 | import path from 'path'; 4 | import { 5 | DEFAULT_API_HOST, 6 | DEFAULT_SERVER_ENDPOINT, 7 | DEFAULT_SERVER_PORT, 8 | ENV_MINIMAX_API_HOST, 9 | ENV_MINIMAX_API_KEY, 10 | ENV_MINIMAX_MCP_BASE_PATH, 11 | ENV_RESOURCE_MODE, 12 | ENV_SERVER_ENDPOINT, 13 | ENV_SERVER_PORT, 14 | ENV_TRANSPORT_MODE, 15 | RESOURCE_MODE_URL, 16 | TRANSPORT_MODE_STDIO, 17 | } from '../const/index.js'; 18 | 19 | /** 20 | * Configuration Manager Class 21 | * Handles priority of various configuration sources: 22 | * 1. Request-level configuration (via meta.auth, per API request) - Highest priority 23 | * 2. Command line arguments - High priority 24 | * 3. Environment variables - Medium priority 25 | * 4. Configuration file - Low priority 26 | * 5. Default values - Lowest priority 27 | */ 28 | export class ConfigManager { 29 | /** 30 | * Get merged configuration 31 | * @param requestConfig Request-level configuration (highest priority) 32 | * @param defaultConfig Default configuration (lowest priority) 33 | * @returns Merged configuration 34 | */ 35 | static getConfig(requestConfig: Partial = {}, defaultConfig: Partial = {}): Config { 36 | // Get user desktop path as default output path 37 | const DesktopPath = process.env[ENV_MINIMAX_MCP_BASE_PATH]!; 38 | 39 | // Create base configuration (lowest priority - 5) 40 | const config: Config = { 41 | apiKey: '', 42 | apiHost: DEFAULT_API_HOST, 43 | basePath: DesktopPath, 44 | resourceMode: RESOURCE_MODE_URL, 45 | server: { 46 | port: DEFAULT_SERVER_PORT, 47 | endpoint: DEFAULT_SERVER_ENDPOINT, 48 | mode: TRANSPORT_MODE_STDIO, 49 | }, 50 | }; 51 | 52 | // Merge default configuration (lowest priority - 5) 53 | if (defaultConfig) { 54 | Object.assign(config, defaultConfig); 55 | } 56 | 57 | // Apply configurations in order from low to high priority 58 | 59 | // 1. Apply configuration from config file (low priority - 4) 60 | this.applyConfigFile(config); 61 | 62 | // 2. Apply configuration from environment variables (medium priority - 3) 63 | this.applyEnvVars(config); 64 | 65 | // 3. Apply configuration from command line arguments (high priority - 2) 66 | this.applyCliArgs(config); 67 | 68 | // 4. Merge request-level configuration (highest priority - 1) 69 | if (requestConfig) { 70 | Object.assign(config, requestConfig); 71 | } 72 | 73 | // console.log(`[${new Date().toISOString()}] Configuration loaded, transport mode: ${config.server?.mode || DEFAULT_TRANSPORT_MODE}`); 74 | 75 | return config; 76 | } 77 | 78 | /** 79 | * Parse command line arguments 80 | * Supports --parameter value format 81 | */ 82 | private static parseCliArgs(): Record { 83 | const args = process.argv.slice(2); // Exclude node and script name 84 | const parsedArgs: Record = {}; 85 | 86 | for (let i = 0; i < args.length; i++) { 87 | // Check if it starts with -- 88 | if (args[i].startsWith('--')) { 89 | const key = args[i].substring(2); // Remove -- prefix 90 | 91 | // Check if the next argument is a value (not starting with --) 92 | if (i + 1 < args.length && !args[i + 1].startsWith('--')) { 93 | parsedArgs[key] = args[i + 1]; 94 | i++; // Skip the next argument as it's already processed 95 | } else { 96 | // Flag parameter without value 97 | parsedArgs[key] = 'true'; 98 | } 99 | } 100 | } 101 | 102 | return parsedArgs; 103 | } 104 | 105 | /** 106 | * Find specific parameter in command line arguments 107 | */ 108 | private static findCliArg(name: string): string | undefined { 109 | const args = this.parseCliArgs(); 110 | 111 | // Support both kebab-case and camelCase 112 | const kebabCase = name.replace(/([A-Z])/g, '-$1').toLowerCase(); 113 | const camelCase = name.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase()); 114 | 115 | return args[name] || args[kebabCase] || args[camelCase]; 116 | } 117 | 118 | /** 119 | * Apply command line arguments to configuration 120 | */ 121 | private static applyCliArgs(config: Config): void { 122 | // Main configuration parameters 123 | const apiKey = this.findCliArg('api-key'); 124 | if (apiKey) { 125 | config.apiKey = apiKey; 126 | } 127 | 128 | const apiHost = this.findCliArg('api-host'); 129 | if (apiHost) { 130 | config.apiHost = apiHost; 131 | } 132 | 133 | const basePath = this.findCliArg('base-path'); 134 | if (basePath) { 135 | config.basePath = basePath; 136 | } 137 | 138 | const resourceMode = this.findCliArg('resource-mode'); 139 | if (resourceMode) { 140 | config.resourceMode = resourceMode; 141 | } 142 | 143 | // Server configuration parameters 144 | if (config.server) { 145 | const port = this.findCliArg('port'); 146 | if (port) { 147 | config.server.port = parseInt(port, 10); 148 | } 149 | 150 | const endpoint = this.findCliArg('endpoint'); 151 | if (endpoint) { 152 | config.server.endpoint = endpoint; 153 | } 154 | 155 | const mode = this.findCliArg('mode'); 156 | if (mode) { 157 | // Ensure mode is a valid TransportMode 158 | if (this.isValidTransportMode(mode)) { 159 | config.server.mode = mode; 160 | } 161 | } 162 | } 163 | } 164 | 165 | /** 166 | * Check if a string is a valid TransportMode 167 | */ 168 | private static isValidTransportMode(mode: string): mode is TransportMode { 169 | return ['stdio', 'rest', 'sse'].includes(mode); 170 | } 171 | 172 | /** 173 | * Apply environment variables to configuration 174 | */ 175 | private static applyEnvVars(config: Config): void { 176 | // Main configuration parameters 177 | if (process.env[ENV_MINIMAX_API_KEY]) { 178 | config.apiKey = process.env[ENV_MINIMAX_API_KEY]; 179 | } 180 | 181 | if (process.env[ENV_MINIMAX_API_HOST]) { 182 | config.apiHost = process.env[ENV_MINIMAX_API_HOST]; 183 | } 184 | 185 | if (process.env[ENV_MINIMAX_MCP_BASE_PATH]) { 186 | config.basePath = process.env[ENV_MINIMAX_MCP_BASE_PATH]; 187 | } 188 | 189 | if (process.env[ENV_RESOURCE_MODE]) { 190 | config.resourceMode = process.env[ENV_RESOURCE_MODE]; 191 | } 192 | 193 | // Server configuration parameters 194 | if (config.server) { 195 | if (process.env[ENV_SERVER_PORT]) { 196 | config.server.port = parseInt(process.env[ENV_SERVER_PORT], 10); 197 | } 198 | 199 | if (process.env[ENV_SERVER_ENDPOINT]) { 200 | config.server.endpoint = process.env[ENV_SERVER_ENDPOINT]; 201 | } 202 | 203 | if (process.env[ENV_TRANSPORT_MODE]) { 204 | const mode = process.env[ENV_TRANSPORT_MODE]; 205 | // Ensure mode is a valid TransportMode 206 | if (this.isValidTransportMode(mode)) { 207 | config.server.mode = mode; 208 | } 209 | } 210 | } 211 | } 212 | 213 | /** 214 | * Apply configuration file to configuration 215 | */ 216 | private static applyConfigFile(config: Config): void { 217 | const configFiles = ['example.minimax-config.json', path.join(process.cwd(), 'example.minimax-config.json')]; 218 | 219 | for (const file of configFiles) { 220 | try { 221 | if (fs.existsSync(file)) { 222 | const fileContent = fs.readFileSync(file, 'utf-8'); 223 | const fileConfig = JSON.parse(fileContent); 224 | 225 | // Merge configuration 226 | this.mergeConfig(config, fileConfig); 227 | // console.log(`[${new Date().toISOString()}] Loaded configuration from file: ${file}`); 228 | break; 229 | } 230 | } catch (error) { 231 | // console.warn(`[${new Date().toISOString()}] Failed to read configuration file: ${file}`, error); 232 | } 233 | } 234 | } 235 | 236 | /** 237 | * Merge configuration objects 238 | */ 239 | private static mergeConfig(target: Config, source: any): void { 240 | if (!source) return; 241 | 242 | // Merge basic configuration 243 | if (source.apiKey) target.apiKey = source.apiKey; 244 | if (source.apiHost) target.apiHost = source.apiHost; 245 | if (source.basePath) target.basePath = source.basePath; 246 | if (source.resourceMode) target.resourceMode = source.resourceMode; 247 | 248 | // Merge server configuration 249 | if (source.server && target.server) { 250 | if (source.server.port) target.server.port = source.server.port; 251 | if (source.server.endpoint) target.server.endpoint = source.server.endpoint; 252 | if (source.server.mode && this.isValidTransportMode(source.server.mode)) { 253 | target.server.mode = source.server.mode; 254 | } 255 | } 256 | } 257 | 258 | /** 259 | * Extract configuration from meta.auth 260 | */ 261 | static extractConfigFromMetaAuth(metaAuth: any): Partial | undefined { 262 | if (!metaAuth || typeof metaAuth !== 'object') { 263 | return undefined; 264 | } 265 | 266 | const config: Partial = {}; 267 | 268 | // Support underscore format 269 | if (metaAuth.api_key) config.apiKey = metaAuth.api_key; 270 | if (metaAuth.api_host) config.apiHost = metaAuth.api_host; 271 | if (metaAuth.base_path) config.basePath = metaAuth.base_path; 272 | if (metaAuth.resource_mode) config.resourceMode = metaAuth.resource_mode; 273 | 274 | // Support camelCase format 275 | if (metaAuth.apiKey) config.apiKey = metaAuth.apiKey; 276 | if (metaAuth.apiHost) config.apiHost = metaAuth.apiHost; 277 | if (metaAuth.basePath) config.basePath = metaAuth.basePath; 278 | if (metaAuth.resourceMode) config.resourceMode = metaAuth.resourceMode; 279 | 280 | // Server configuration 281 | const server: any = {}; 282 | let hasServerConfig = false; 283 | 284 | // Underscore format 285 | if (metaAuth.server_port) { 286 | server.port = parseInt(metaAuth.server_port, 10); 287 | hasServerConfig = true; 288 | } 289 | if (metaAuth.server_endpoint) { 290 | server.endpoint = metaAuth.server_endpoint; 291 | hasServerConfig = true; 292 | } 293 | if (metaAuth.server_mode) { 294 | const mode = metaAuth.server_mode; 295 | if (this.isValidTransportMode(mode)) { 296 | server.mode = mode; 297 | hasServerConfig = true; 298 | } 299 | } 300 | 301 | // CamelCase format 302 | if (metaAuth.serverPort) { 303 | server.port = parseInt(metaAuth.serverPort, 10); 304 | hasServerConfig = true; 305 | } 306 | if (metaAuth.serverEndpoint) { 307 | server.endpoint = metaAuth.serverEndpoint; 308 | hasServerConfig = true; 309 | } 310 | if (metaAuth.serverMode) { 311 | const mode = metaAuth.serverMode; 312 | if (this.isValidTransportMode(mode)) { 313 | server.mode = mode; 314 | hasServerConfig = true; 315 | } 316 | } 317 | 318 | // Only add server object if there are server configurations 319 | if (hasServerConfig) { 320 | config.server = server; 321 | } 322 | 323 | return Object.keys(config).length > 0 ? config : undefined; 324 | } 325 | } 326 | -------------------------------------------------------------------------------- /README.zh-CN.md: -------------------------------------------------------------------------------- 1 | ![export](https://github.com/MiniMax-AI/MiniMax-01/raw/main/figures/MiniMaxLogo-Light.png) 2 | 3 |
4 | 5 | # MiniMax MCP JS 6 | 7 | MiniMax MCP JS 是 MiniMax MCP 的 JavaScript/TypeScript 实现,提供图像生成、视频生成、文本转语音等功能。 8 | 9 | 23 | 24 | 35 | 36 |
37 | 38 | Code License 39 | 40 |
41 | 42 |
43 | 44 | ## 文档 45 | 46 | - [English Documentation](README.md) 47 | - [Python 版本](https://github.com/MiniMax-AI/MiniMax-MCP) - MiniMax MCP的官方Python实现 48 | 49 | ## 更新日志 50 | 51 | ### 2025年7月22日 52 | 53 | #### 🔧 修复与优化 54 | - **TTS工具修复**: 修复了 `text_to_audio` 工具中 `languageBoost` 和 `subtitleEnable` 参数的处理问题 55 | - **API响应增强**: TTS API 可以返回音频文件(audio)和字幕文件(subtitle_file),提供更完整的语音转文字体验 56 | 57 | ### 2025年7月7日 58 | 59 | #### 🆕 新增功能 60 | - **音色设计**: 新增 `voice_design` 工具 - 根据描述性提示词创建自定义音色并生成试听音频 61 | - **视频生成增强**: 新增 `MiniMax-Hailuo-02` 模型,支持超清画质和时长/分辨率控制 62 | - **音乐生成**: 采用 `music-1.5` 模型增强 `music_generation` 工具 63 | 64 | #### 📈 功能增强 65 | - `voice_design` - 根据文本描述生成个性化音色 66 | - `generate_video` - 现在支持 MiniMax-Hailuo-02 模型,可选择 6s/10s 时长和 768P/1080P 分辨率 67 | - `music_generation` - 采用 music-1.5 模型进行高质量音乐创作 68 | 69 | ## 功能特性 70 | 71 | - 文本转语音 (TTS) 72 | - 图像生成 73 | - 视频生成 74 | - 语音克隆 75 | - 音乐生成 76 | - 音色设计 77 | - 动态配置(支持环境变量和请求参数) 78 | - 兼容MCP平台托管(ModelScope和其他MCP平台) 79 | 80 | ## 安装 81 | 82 | ```bash 83 | # 使用 pnpm 安装(推荐) 84 | pnpm add minimax-mcp-js 85 | ``` 86 | 87 | ## 快速开始 88 | 89 | MiniMax MCP JS 实现了 [Model Context Protocol (MCP)](https://github.com/anthropics/model-context-protocol) 规范,可以作为服务器与支持 MCP 的客户端(如 Claude AI)进行交互。 90 | 91 | ### 使用 MCP 客户端的快速开始 92 | 93 | 1. 从[MiniMax国内开放平台](https://platform.minimaxi.com/user-center/basic-information/interface-key)或[MiniMax国际开放平台](https://www.minimax.io/platform/user-center/basic-information/interface-key)获取您的 API 密钥。 94 | 2. 确保你已经安装了 [Node.js 和 npm](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm) 95 | 3. **重要提示: API的服务器地址和密钥在不同区域有所不同**,两者需要匹配,否则会有 `invalid api key` 的错误 96 | 97 | |地区| 国际 | 国内 | 98 | |:--|:-----|:-----| 99 | |MINIMAX_API_KEY| 获取密钥 [MiniMax国际版](https://www.minimax.io/platform/user-center/basic-information/interface-key) | 获取密钥 [MiniMax](https://platform.minimaxi.com/user-center/basic-information/interface-key) | 100 | |MINIMAX_API_HOST| ​https://api.minimaxi.chat (请注意额外的 **"i"** 字母) | ​https://api.minimax.chat | 101 | 102 | ### 通过 MCP 客户端使用(推荐) 103 | 104 | 在 MCP 客户端中配置: 105 | 106 | #### Claude Desktop 107 | 108 | 进入 `Claude > Settings > Developer > Edit Config > claude_desktop_config.json` 添加如下配置: 109 | 110 | ```json 111 | { 112 | "mcpServers": { 113 | "minimax-mcp-js": { 114 | "command": "npx", 115 | "args": [ 116 | "-y", 117 | "minimax-mcp-js" 118 | ], 119 | "env": { 120 | "MINIMAX_API_HOST": "https://api.minimax.chat", 121 | "MINIMAX_API_KEY": "<您的API密钥>", 122 | "MINIMAX_MCP_BASE_PATH": "<本地输出目录路径,如/User/xxx/Desktop>", 123 | "MINIMAX_RESOURCE_MODE": "<可选配置,资源生成后的提供方式, [url|local], 默认为 url>" 124 | } 125 | } 126 | } 127 | } 128 | ``` 129 | 130 | #### Cursor 131 | 132 | 进入 `Cursor → Preferences → Cursor Settings → MCP → Add new global MCP Server` 添加上述配置。 133 | 134 | ⚠️ **注意**: 如果您在 Cursor 中使用 MiniMax MCP JS 时遇到 "No tools found" 错误,请将 Cursor 升级到最新版本。 135 | 更多信息,请参阅这个[讨论帖](https://forum.cursor.com/t/mcp-servers-no-tools-found/49094/23). 136 | 137 | 完成以上步骤后,您的MCP客户端就可以通过这些工具与MiniMax进行交互了。 138 | 139 | **本地开发**: 140 | 在本地开发时,您可以使用 `npm link` 来测试您的更改: 141 | ```bash 142 | # 在您的项目目录中 143 | npm link 144 | ``` 145 | 146 | ⚠️ **注意**:API密钥需要与主机地址匹配,在国际版和中国大陆版使用不同的主机地址: 147 | - 全球版主机地址: `https://api.minimaxi.chat` (注意多了一个 "i") 148 | - 中国大陆版主机地址: `https://api.minimax.chat` 149 | 150 | ## 传输模式 151 | 152 | MiniMax MCP JS 支持三种传输模式: 153 | 154 | | 特性 | stdio (默认) | REST | SSE | 155 | |:-----|:-----|:-----|:-----| 156 | | 运行环境 | 本地运行 | 可本地或云端部署 | 可本地或云端部署 | 157 | | 通信方式 | 通过`标准输入输出`通信 | 通过`HTTP请求`通信 | 通过`服务器发送事件`通信 | 158 | | 适用场景 | 本地MCP客户端集成 | API服务,跨语言调用 | 需要服务器推送的应用 | 159 | | 输入限制 | 支持处理`本地文件`或有效的`URL`资源 | 当部署在云端时,建议使用`URL`作为输入 | 当部署在云端时,建议使用`URL`作为输入 | 160 | 161 | 162 | ## 配置方式 163 | 164 | MiniMax-MCP-JS 提供了多种灵活的配置方式,以适应不同的使用场景。配置的优先级从高到低排列如下: 165 | 166 | ### 1. 请求参数配置 (最高优先级) 167 | 168 | 在平台托管环境(如ModelScope或其他MCP平台)中,可以通过请求参数中的`meta.auth`对象为每个请求提供独立的配置: 169 | 170 | ```json 171 | { 172 | "params": { 173 | "meta": { 174 | "auth": { 175 | "api_key": "您的API密钥", 176 | "api_host": "https://api.minimax.chat", 177 | "base_path": "/输出路径", 178 | "resource_mode": "url" 179 | } 180 | } 181 | } 182 | } 183 | ``` 184 | 185 | 这种方式允许多租户使用,每个请求可以使用不同的API密钥和配置。 186 | 187 | ### 2. API配置 188 | 189 | 当在其他项目中作为模块使用时,可以通过`startMiniMaxMCP`函数传入配置: 190 | 191 | ```javascript 192 | import { startMiniMaxMCP } from 'minimax-mcp-js'; 193 | 194 | await startMiniMaxMCP({ 195 | apiKey: '您的API密钥', 196 | apiHost: 'https://api.minimax.chat', 197 | basePath: '/输出路径', 198 | resourceMode: 'url' 199 | }); 200 | ``` 201 | 202 | ### 3. 命令行参数 203 | 204 | 1. 全局安装 CLI 工具: 205 | ```bash 206 | # 全局安装 207 | pnpm install -g minimax-mcp-js 208 | ``` 209 | 210 | 2. 当作为CLI工具使用时,可以通过命令行参数提供配置: 211 | 212 | ```bash 213 | minimax-mcp-js --api-key 您的API密钥 --api-host https://api.minimax.chat --base-path /输出路径 --resource-mode url 214 | ``` 215 | 216 | ### 4. 环境变量 (最低优先级) 217 | 218 | 最基本的配置方式,通过环境变量提供: 219 | 220 | ```bash 221 | # MiniMax API 密钥 (必需) 222 | MINIMAX_API_KEY=您的API密钥 223 | 224 | # 输出文件的基础路径 (可选,默认为用户桌面) 225 | MINIMAX_MCP_BASE_PATH=~/Desktop 226 | 227 | # MiniMax API 主机 (可选,默认为 https://api.minimax.chat) 228 | MINIMAX_API_HOST=https://api.minimax.chat 229 | 230 | # 资源模式 (可选,默认为 'url') 231 | # 选项: 'url' (返回URL), 'local' (本地保存文件) 232 | MINIMAX_RESOURCE_MODE=url 233 | ``` 234 | 235 | ## 配置优先级 236 | 237 | 当使用多种配置方式时,将按照以下优先级顺序应用(从高到低): 238 | 239 | 1. **请求级配置**(通过每个API请求的`meta.auth`字段) 240 | 2. **命令行参数** 241 | 3. **环境变量** 242 | 4. **配置文件** 243 | 5. **默认值** 244 | 245 | 这种优先级设计确保了在不同部署场景下的灵活性,同时为多租户环境提供了按请求配置的能力。 246 | 247 | ## 配置项说明 248 | 249 | | 配置项 | 描述 | 默认值 | 250 | |-------|------|--------| 251 | | apiKey | MiniMax API 密钥 | 无(必填) | 252 | | apiHost | MiniMax API 主机地址 | https://api.minimax.chat | 253 | | basePath | 输出文件的基础路径 | 用户桌面 | 254 | | resourceMode | 资源处理模式,'url' 或 'local' | url | 255 | 256 | ⚠️ **注意**:API密钥需要与主机地址匹配,在国际版和中国大陆版使用不同的主机地址: 257 | - 全球版主机地址: `https://api.minimaxi.chat` (注意多了一个 "i") 258 | - 中国大陆版主机地址: `https://api.minimax.chat` 259 | 260 | 261 | ## 使用示例 262 | 263 | ⚠️ 注意:使用这些工具可能会产生费用。 264 | 265 | ### 1. 播报晚间新闻片段 266 | 267 | 268 | ### 2. 克隆声音 269 | 270 | 271 | ### 3. 生成视频 272 | 273 | 274 | 275 | ### 4. 生成图像 276 | 277 | 278 | 279 | ### 5. 生成音乐 280 | 281 | 282 | ### 6. 音色设计 283 | 284 | 285 | ## 可用工具 286 | 287 | ### 文本转语音 288 | 289 | 将文本转换为语音文件。 290 | 291 | 工具名称:`text_to_audio` 292 | 293 | 参数: 294 | - `text`: 要转换的文本 (必需) 295 | - `model`: 模型版本,选项为 'speech-02-hd', 'speech-02-turbo', 'speech-01-hd', 'speech-01-turbo', 'speech-01-240228', 'speech-01-turbo-240228',默认为 'speech-02-hd' 296 | - `voiceId`: 语音 ID,默认为 'male-qn-qingse' 297 | - `speed`: 语速,范围 0.5-2.0,默认为 1.0 298 | - `vol`: 音量,范围 0.1-10.0,默认为 1.0 299 | - `pitch`: 音调,范围 -12 到 12,默认为 0 300 | - `emotion`: 情感,选项为 'happy', 'sad', 'angry', 'fearful', 'disgusted', 'surprised', 'neutral',默认为 'happy'。注意:此参数仅对 'speech-02-hd', 'speech-02-turbo', 'speech-01-turbo', 'speech-01-hd' 模型有效 301 | - `format`: 音频格式,选项为 'mp3', 'pcm', 'flac', 'wav',默认为 'mp3' 302 | - `sampleRate`: 采样率 (Hz),选项为 8000, 16000, 22050, 24000, 32000, 44100,默认为 32000 303 | - `bitrate`: 比特率 (bps),选项为 64000, 96000, 128000, 160000, 192000, 224000, 256000, 320000,默认为 128000 304 | - `channel`: 音频通道数,选项为 1 或 2,默认为 1 305 | - `languageBoost`: 增强对指定的小语种和方言的识别能力,设置后可以提升在指定小语种/方言场景下的语音表现。如果不明确小语种类型,则可以选择"auto",模型将自主判断小语种类型。支持以下取值: 306 | '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',默认为 'auto' 307 | - `stream`: 启用流式输出 308 | - `subtitleEnable`: 控制是否开启字幕服务的开关。此参数仅对 'speech-01-turbo' 和 'speech-01-hd' 模型生效。默认为false 309 | - `outputDirectory`: 保存输出文件的目录。 `outputDirectory` 是相对于 `MINIMAX_MCP_BASE_PATH`(或配置中的 `basePath`)的。最终的保存路径是 `${basePath}/${outputDirectory}`, 例如, 如果 `MINIMAX_MCP_BASE_PATH=~/Desktop` 且 `outputDirectory=workspace`,则输出将被保存到 `~/Desktop/workspace/` (可选) 310 | - `outputFile`: 保存输出文件的路径 (可选,如果未提供则自动生成) 311 | 312 | ### 语音克隆 313 | 314 | 从音频文件克隆语音。 315 | 316 | 工具名称:`voice_clone` 317 | 318 | 参数: 319 | - `audioFile`: 音频文件路径 (必需) 320 | - `voiceId`: 语音 ID (必需) 321 | - `text`: 演示音频的文本 (可选) 322 | - `outputDirectory`: 保存输出文件的目录。 `outputDirectory` 是相对于 `MINIMAX_MCP_BASE_PATH`(或配置中的 `basePath`)的。最终的保存路径是 `${basePath}/${outputDirectory}`, 例如, 如果 `MINIMAX_MCP_BASE_PATH=~/Desktop` 且 `outputDirectory=workspace`,则输出将被保存到 `~/Desktop/workspace/` (可选) 323 | 324 | ### 列出所有语音类型 325 | 326 | 列出所有可用的文本转语音声音。仅在 api_host 为 https://api.minimax.chat 时支持。 327 | 328 | 工具名称:`list_voices` 329 | 330 | 参数: 331 | - `voiceType`: 要列出的语音类型,选项为 'all'(全部), 'system'(系统), 'voice_cloning'(克隆语音),默认为 'all' 332 | 333 | ### 播放音频 334 | 335 | 播放音频文件。支持 WAV 和 MP3 格式。不支持视频。 336 | 337 | 工具名称:`play_audio` 338 | 339 | 参数: 340 | - `inputFilePath`: 要播放的音频文件路径 (必需) 341 | - `isUrl`: 音频文件是否为 URL,默认为 false 342 | 343 | ### 文本生成图像 344 | 345 | 根据文本提示生成图像。 346 | 347 | 工具名称:`text_to_image` 348 | 349 | 参数: 350 | - `prompt`: 图像描述 (必需) 351 | - `model`: 模型版本,默认为 'image-01' 352 | - `aspectRatio`: 宽高比,默认为 '1:1',选项为 '1:1', '16:9','4:3', '3:2', '2:3', '3:4', '9:16', '21:9' 353 | - `n`: 生成图像数量,范围 1-9,默认为 1 354 | - `promptOptimizer`: 是否优化提示,默认为 true 355 | - `subjectReference`: 角色参考的本地图像文件路径或公共 URL (可选) 356 | - `outputDirectory`: 保存输出文件的目录。 `outputDirectory` 是相对于 `MINIMAX_MCP_BASE_PATH`(或配置中的 `basePath`)的。最终的保存路径是 `${basePath}/${outputDirectory}`, 例如, 如果 `MINIMAX_MCP_BASE_PATH=~/Desktop` 且 `outputDirectory=workspace`,则输出将被保存到 `~/Desktop/workspace/` (可选) 357 | - `outputFile`: 保存输出文件的路径 (可选,如果未提供则自动生成) 358 | 359 | ### 生成视频 360 | 361 | 根据文本提示生成视频。 362 | 363 | 工具名称:`generate_video` 364 | 365 | 参数: 366 | - `prompt`: 视频描述 (必需) 367 | - `model`: 模型版本,选项为 'T2V-01', 'T2V-01-Director', 'I2V-01', 'I2V-01-Director', 'I2V-01-live', 'S2V-01', 'MiniMax-Hailuo-02', 默认为 'MiniMax-Hailuo-02' 368 | - `firstFrameImage`: 第一帧图像路径 (可选) 369 | - `duration`: 视频时长秒数。模型必须是 "MiniMax-Hailuo-02"。值可以是 6 和 10。(可选) 370 | - `resolution`: 视频分辨率。模型必须是 "MiniMax-Hailuo-02"。值范围为 ["768P", "1080P"]。(可选) 371 | - `outputDirectory`: 保存输出文件的目录。 `outputDirectory` 是相对于 `MINIMAX_MCP_BASE_PATH`(或配置中的 `basePath`)的。最终的保存路径是 `${basePath}/${outputDirectory}`, 例如, 如果 `MINIMAX_MCP_BASE_PATH=~/Desktop` 且 `outputDirectory=workspace`,则输出将被保存到 `~/Desktop/workspace/` (可选) 372 | - `outputFile`: 保存输出文件的路径 (可选,如果未提供则自动生成) 373 | - `asyncMode`: 是否使用异步模式。默认为 False。如果为 True,视频生成任务将异步提交并返回任务 ID。需要使用 `query_video_generation` 工具来检查任务状态并获取结果。(可选) 374 | 375 | ### 查询视频生成状态 376 | 377 | 查询视频生成任务的状态。 378 | 379 | 工具名称:`query_video_generation` 380 | 381 | 参数: 382 | - `taskId`: 要查询的任务 ID。如果 `generate_video` 工具的 `async_mode` 为 True,则应使用其返回的 task_id。(必需) 383 | - `outputDirectory`: 保存输出文件的目录。 `outputDirectory` 是相对于 `MINIMAX_MCP_BASE_PATH`(或配置中的 `basePath`)的。最终的保存路径是 `${basePath}/${outputDirectory}`, 例如, 如果 `MINIMAX_MCP_BASE_PATH=~/Desktop` 且 `outputDirectory=workspace`,则输出将被保存到 `~/Desktop/workspace/` (可选) 384 | 385 | ### 音乐生成 386 | 387 | 根据提示和歌词生成音乐。 388 | 389 | 工具名称:`music_generation` 390 | 391 | 参数: 392 | - `prompt`: 音乐创作灵感,描述风格、情绪、场景等。例如:"流行音乐,悲伤,适合雨夜"。字符范围:[10, 300]。(必需) 393 | - `lyrics`: 用于音乐生成的歌词。使用换行符 (\\n) 分隔每行歌词。支持歌词结构标签 [Intro] [Verse] [Chorus] [Bridge] [Outro] 以增强音乐性。字符范围:[10, 600](每个中文字符、标点符号和字母计为1个字符)。(必需) 394 | - `sampleRate`: 生成音乐的采样率。值:[16000, 24000, 32000, 44100],默认为 32000。(可选) 395 | - `bitrate`: 生成音乐的比特率。值:[32000, 64000, 128000, 256000],默认为 128000。(可选) 396 | - `format`: 生成音乐的格式。值:["mp3", "wav", "pcm"],默认为 'mp3'。(可选) 397 | - `outputDirectory`: 保存输出文件的目录。 `outputDirectory` 是相对于 `MINIMAX_MCP_BASE_PATH`(或配置中的 `basePath`)的。最终的保存路径是 `${basePath}/${outputDirectory}`, 例如, 如果 `MINIMAX_MCP_BASE_PATH=~/Desktop` 且 `outputDirectory=workspace`,则输出将被保存到 `~/Desktop/workspace/` (可选) 398 | 399 | 400 | ### 音色设计 401 | 402 | 根据描述提示生成音色。 403 | 404 | 工具名称:`voice_design` 405 | 406 | 参数: 407 | - `prompt`: 生成音色的提示。(必需) 408 | - `previewText`: 预览音色的文本。(必需) 409 | - `voiceId`: 要使用的音色ID。例如:"male-qn-qingse"/"audiobook_female_1"/"cute_boy"/"Charming_Lady"...(可选) 410 | - `outputDirectory`: 保存输出文件的目录。 `outputDirectory` 是相对于 `MINIMAX_MCP_BASE_PATH`(或配置中的 `basePath`)的。最终的保存路径是 `${basePath}/${outputDirectory}`, 例如, 如果 `MINIMAX_MCP_BASE_PATH=~/Desktop` 且 `outputDirectory=workspace`,则输出将被保存到 `~/Desktop/workspace/` (可选) 411 | 412 | ## 常见问题 413 | 414 | ### 1. 如何使用 `generate_video` 的异步模式 415 | 在对话开始之前定义完成规则: 416 | 417 | 或者,可以在你的本地客户端的配置中设置这些规则(例如 Cursor): 418 | 419 | 420 | ## 开发 421 | 422 | ### 设置 423 | 424 | ```bash 425 | # 克隆仓库 426 | git clone https://github.com/MiniMax-AI/MiniMax-MCP-JS.git 427 | cd minimax-mcp-js 428 | 429 | # 安装依赖 430 | pnpm install 431 | ``` 432 | 433 | ### 构建 434 | 435 | ```bash 436 | # 构建项目 437 | pnpm run build 438 | ``` 439 | 440 | ### 运行 441 | 442 | ```bash 443 | # 运行 MCP 服务器 444 | pnpm start 445 | ``` 446 | 447 | ## 许可证 448 | 449 | MIT 450 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![export](https://github.com/MiniMax-AI/MiniMax-01/raw/main/figures/MiniMaxLogo-Light.png) 2 | 3 |
4 | 5 | # MiniMax MCP JS 6 | 7 | JavaScript/TypeScript implementation of MiniMax MCP, providing image generation, video generation, text-to-speech, and more. 8 | 9 | 23 | 24 | 35 | 36 |
37 | 38 | Code License 39 | 40 | Smithery Badge 41 |
42 | 43 |
44 | 45 | ## Documentation 46 | 47 | - [中文文档](README.zh-CN.md) 48 | - [Python Version](https://github.com/MiniMax-AI/MiniMax-MCP) - Official Python implementation of MiniMax MCP 49 | 50 | ## Release Notes 51 | 52 | ### July 22, 2025 53 | 54 | #### 🔧 Fixes & Improvements 55 | - **TTS Tool Fixes**: Fixed parameter handling for `languageBoost` and `subtitleEnable` in the `text_to_audio` tool 56 | - **API Response Enhancement**: TTS API can return both audio file and subtitle file, providing a more complete speech-to-text experience 57 | 58 | ### July 7, 2025 59 | 60 | #### 🆕 What's New 61 | - **Voice Design**: New `voice_design` tool - create custom voices from descriptive prompts with preview audio 62 | - **Video Enhancement**: Added `MiniMax-Hailuo-02` model with ultra-clear quality and duration/resolution controls 63 | - **Music Generation**: Enhanced `music_generation` tool powered by `music-1.5` model 64 | 65 | #### 📈 Enhanced Tools 66 | - `voice_design` - Generate personalized voices from text descriptions 67 | - `generate_video` - Now supports MiniMax-Hailuo-02 with 6s/10s duration and 768P/1080P resolution options 68 | - `music_generation` - High-quality music creation with music-1.5 model 69 | 70 | ## Features 71 | 72 | - Text-to-Speech (TTS) 73 | - Image Generation 74 | - Video Generation 75 | - Voice Cloning 76 | - Music Generation 77 | - Voice Design 78 | - Dynamic configuration (supports both environment variables and request parameters) 79 | - Compatible with MCP platform hosting (ModelScope and other MCP platforms) 80 | 81 | ## Installation 82 | 83 | ### Installing via Smithery 84 | 85 | To install MiniMax MCP JS for Claude Desktop automatically via [Smithery](https://smithery.ai/server/@MiniMax-AI/MiniMax-MCP-JS): 86 | 87 | ```bash 88 | npx -y @smithery/cli install @MiniMax-AI/MiniMax-MCP-JS --client claude 89 | ``` 90 | 91 | ### Installing manually 92 | ```bash 93 | # Install with pnpm (recommended) 94 | pnpm add minimax-mcp-js 95 | ``` 96 | 97 | ## Quick Start 98 | 99 | MiniMax MCP JS implements the [Model Context Protocol (MCP)](https://github.com/anthropics/model-context-protocol) specification and can be used as a server to interact with MCP-compatible clients (such as Claude AI). 100 | 101 | ### Quickstart with MCP Client 102 | 103 | 1. Get your API key from [MiniMax International Platform](https://www.minimax.io/platform/user-center/basic-information/interface-key). 104 | 2. Make sure that you already installed [Node.js and npm](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm) 105 | 3. **Important: API HOST&KEY are different in different region**, they must match, otherwise you will receive an `Invalid API key` error. 106 | 107 | |Region| Global | Mainland | 108 | |:--|:-----|:-----| 109 | |MINIMAX_API_KEY| go get from [MiniMax Global](https://www.minimax.io/platform/user-center/basic-information/interface-key) | go get from [MiniMax](https://platform.minimaxi.com/user-center/basic-information/interface-key) | 110 | |MINIMAX_API_HOST| ​https://api.minimaxi.chat (note the extra **"i"**) | ​https://api.minimax.chat | 111 | 112 | 113 | ### Using with MCP Clients (Recommended) 114 | 115 | Configure your MCP client: 116 | 117 | #### Claude Desktop 118 | 119 | Go to `Claude > Settings > Developer > Edit Config > claude_desktop_config.json` to include: 120 | 121 | ```json 122 | { 123 | "mcpServers": { 124 | "minimax-mcp-js": { 125 | "command": "npx", 126 | "args": [ 127 | "-y", 128 | "minimax-mcp-js" 129 | ], 130 | "env": { 131 | "MINIMAX_API_HOST": "", 132 | "MINIMAX_API_KEY": "", 133 | "MINIMAX_MCP_BASE_PATH": "", 134 | "MINIMAX_RESOURCE_MODE": "" 135 | } 136 | } 137 | } 138 | } 139 | ``` 140 | 141 | #### Cursor 142 | 143 | Go to `Cursor → Preferences → Cursor Settings → MCP → Add new global MCP Server` to add the above config. 144 | 145 | ⚠️ **Note**: If you encounter a "No tools found" error when using MiniMax MCP JS with Cursor, please update your Cursor to the latest version. For more information, see this [discussion thread](https://forum.cursor.com/t/mcp-servers-no-tools-found/49094/23). 146 | 147 | That's it. Your MCP client can now interact with MiniMax through these tools. 148 | 149 | **For local development**: 150 | When developing locally, you can use `npm link` to test your changes: 151 | ```bash 152 | # In your project directory 153 | npm link 154 | ``` 155 | 156 | Then configure Claude Desktop or Cursor to use npx as shown above. This will automatically use your linked version. 157 | 158 | ⚠️ **Note**: The API key needs to match the host address. Different hosts are used for global and mainland China versions: 159 | - Global Host: `https://api.minimaxi.chat` (note the extra "i") 160 | - Mainland China Host: `https://api.minimaxi.chat` 161 | 162 | ## Transport Modes 163 | 164 | MiniMax MCP JS supports three transport modes: 165 | 166 | | Feature | stdio (default) | REST | SSE | 167 | |:-----|:-----|:-----|:-----| 168 | | Environment | Local only | Local or cloud deployment | Local or cloud deployment | 169 | | Communication | Via `standard I/O` | Via `HTTP requests` | Via `server-sent events` | 170 | | Use Cases | Local MCP client integration | API services, cross-language calls | Applications requiring server push | 171 | | Input Restrictions | Supports `local files` or `URL` resources | When deployed in cloud, `URL` input recommended | When deployed in cloud, `URL` input recommended | 172 | 173 | ## Configuration 174 | 175 | MiniMax-MCP-JS provides multiple flexible configuration methods to adapt to different use cases. The configuration priority from highest to lowest is as follows: 176 | 177 | ### 1. Request Parameter Configuration (Highest Priority) 178 | 179 | In platform hosting environments (like ModelScope or other MCP platforms), you can provide an independent configuration for each request via the `meta.auth` object in the request parameters: 180 | 181 | ```json 182 | { 183 | "params": { 184 | "meta": { 185 | "auth": { 186 | "api_key": "your_api_key_here", 187 | "api_host": "", 188 | "base_path": "/path/to/output", 189 | "resource_mode": "url" 190 | } 191 | } 192 | } 193 | } 194 | ``` 195 | 196 | This method enables multi-tenant usage, where each request can use different API keys and configurations. 197 | 198 | ### 2. API Configuration 199 | 200 | When used as a module in other projects, you can pass configuration through the `startMiniMaxMCP` function: 201 | 202 | ```javascript 203 | import { startMiniMaxMCP } from 'minimax-mcp-js'; 204 | 205 | await startMiniMaxMCP({ 206 | apiKey: 'your_api_key_here', 207 | apiHost: 'https://api.minimaxi.chat', // Global Host - https://api.minimaxi.chat, Mainland Host - https://api.minimax.chat 208 | basePath: '/path/to/output', 209 | resourceMode: 'url' 210 | }); 211 | ``` 212 | 213 | ### 3. Command Line Arguments 214 | 215 | 1. Install the CLI tool globally: 216 | ```bash 217 | # Install globally 218 | pnpm install -g minimax-mcp-js 219 | ``` 220 | 221 | 2. When used as a CLI tool, you can provide configuration via command line arguments: 222 | 223 | ```bash 224 | minimax-mcp-js --api-key your_api_key_here --api-host https://api.minimaxi.chat --base-path /path/to/output --resource-mode url 225 | ``` 226 | 227 | ### 4. Environment Variables (Lowest Priority) 228 | 229 | The most basic configuration method is through environment variables: 230 | 231 | ```bash 232 | # MiniMax API Key (required) 233 | MINIMAX_API_KEY=your_api_key_here 234 | 235 | # Base path for output files (optional, defaults to user's desktop) 236 | MINIMAX_MCP_BASE_PATH=~/Desktop 237 | 238 | # MiniMax API Host (optional, defaults to https://api.minimaxi.chat, Global Host - https://api.minimaxi.chat, Mainland Host - https://api.minimax.chat) 239 | MINIMAX_API_HOST=https://api.minimaxi.chat 240 | 241 | # Resource mode (optional, defaults to 'url') 242 | # Options: 'url' (return URLs), 'local' (save files locally) 243 | MINIMAX_RESOURCE_MODE=url 244 | ``` 245 | 246 | ### Configuration Priority 247 | 248 | When multiple configuration methods are used, the following priority order applies (from highest to lowest): 249 | 250 | 1. **Request-level configuration** (via `meta.auth` in each API request) 251 | 2. **Command line arguments** 252 | 3. **Environment variables** 253 | 4. **Configuration file** 254 | 5. **Default values** 255 | 256 | This prioritization ensures flexibility across different deployment scenarios while maintaining per-request configuration capabilities for multi-tenant environments. 257 | 258 | ### Configuration Parameters 259 | 260 | | Parameter | Description | Default Value | 261 | |-----------|-------------|---------------| 262 | | apiKey | MiniMax API Key | None (Required) | 263 | | apiHost | MiniMax API Host | Global Host - https://api.minimaxi.chat, Mainland Host - https://api.minimax.chat | 264 | | basePath | Base path for output files | User's desktop | 265 | | resourceMode | Resource handling mode, 'url' or 'local' | url | 266 | 267 | ⚠️ **Note**: The API key needs to match the host address. Different hosts are used for global and mainland China versions: 268 | - Global Host: `https://api.minimaxi.chat` (note the extra "i") 269 | - Mainland China Host: `https://api.minimax.chat` 270 | 271 | ## Example usage 272 | 273 | ⚠️ Warning: Using these tools may incur costs. 274 | 275 | ### 1. broadcast a segment of the evening news 276 | 277 | 278 | ### 2. clone a voice 279 | 280 | 281 | ### 3. generate a video 282 | 283 | 284 | 285 | ### 4. generate images 286 | 287 | 288 | 289 | ### 5. generate music 290 | 291 | 292 | ### 6. voice design 293 | 294 | 295 | ## Available Tools 296 | 297 | ### Text to Audio 298 | 299 | Convert text to speech audio file. 300 | 301 | Tool Name: `text_to_audio` 302 | 303 | Parameters: 304 | - `text`: Text to convert (required) 305 | - `model`: Model version, options are 'speech-02-hd', 'speech-02-turbo', 'speech-01-hd', 'speech-01-turbo', 'speech-01-240228', 'speech-01-turbo-240228', default is 'speech-02-hd' 306 | - `voiceId`: Voice ID, default is 'male-qn-qingse' 307 | - `speed`: Speech speed, range 0.5-2.0, default is 1.0 308 | - `vol`: Volume, range 0.1-10.0, default is 1.0 309 | - `pitch`: Pitch, range -12 to 12, default is 0 310 | - `emotion`: Emotion, options are 'happy', 'sad', 'angry', 'fearful', 'disgusted', 'surprised', 'neutral', default is 'happy'. Note: This parameter only works with 'speech-02-hd', 'speech-02-turbo', 'speech-01-turbo', 'speech-01-hd' models 311 | - `format`: Audio format, options are 'mp3', 'pcm', 'flac', 'wav', default is 'mp3' 312 | - `sampleRate`: Sample rate (Hz), options are 8000, 16000, 22050, 24000, 32000, 44100, default is 32000 313 | - `bitrate`: Bitrate (bps), options are 64000, 96000, 128000, 160000, 192000, 224000, 256000, 320000, default is 128000 314 | - `channel`: Audio channels, options are 1 or 2, default is 1 315 | - `languageBoost`: Enhance the ability to recognize specified languages and dialects. 316 | Supported values include: 317 | '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' 318 | - `stream`: Enable streaming output 319 | - `subtitleEnable`: 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 320 | - `outputDirectory`: Directory to save the output file. `outputDirectory` is relative to `MINIMAX_MCP_BASE_PATH` (or `basePath` in config). The final save path is `${basePath}/${outputDirectory}`. For example, if `MINIMAX_MCP_BASE_PATH=~/Desktop` and `outputDirectory=workspace`, the output will be saved to `~/Desktop/workspace/`. (optional) 321 | - `outputFile`: Path to save the output file (optional, auto-generated if not provided) 322 | 323 | ### Play Audio 324 | 325 | Play an audio file. Supports WAV and MP3 formats. Does not support video. 326 | 327 | Tool Name: `play_audio` 328 | 329 | Parameters: 330 | - `inputFilePath`: Path to the audio file to play (required) 331 | - `isUrl`: Whether the audio file is a URL, default is false 332 | 333 | ### Voice Clone 334 | 335 | Clone a voice from an audio file. 336 | 337 | Tool Name: `voice_clone` 338 | 339 | Parameters: 340 | - `audioFile`: Path to audio file (required) 341 | - `voiceId`: Voice ID (required) 342 | - `text`: Text for demo audio (optional) 343 | - `outputDirectory`: Directory to save the output file. `outputDirectory` is relative to `MINIMAX_MCP_BASE_PATH` (or `basePath` in config). The final save path is `${basePath}/${outputDirectory}`. For example, if `MINIMAX_MCP_BASE_PATH=~/Desktop` and `outputDirectory=workspace`, the output will be saved to `~/Desktop/workspace/`. (optional) 344 | 345 | ### Text to Image 346 | 347 | Generate images based on text prompts. 348 | 349 | Tool Name: `text_to_image` 350 | 351 | Parameters: 352 | - `prompt`: Image description (required) 353 | - `model`: Model version, default is 'image-01' 354 | - `aspectRatio`: Aspect ratio, default is '1:1', options are '1:1', '16:9','4:3', '3:2', '2:3', '3:4', '9:16', '21:9' 355 | - `n`: Number of images to generate, range 1-9, default is 1 356 | - `promptOptimizer`: Whether to optimize the prompt, default is true 357 | - `subjectReference`: Path to local image file or public URL for character reference (optional) 358 | - `outputDirectory`: Directory to save the output file. `outputDirectory` is relative to `MINIMAX_MCP_BASE_PATH` (or `basePath` in config). The final save path is `${basePath}/${outputDirectory}`. For example, if `MINIMAX_MCP_BASE_PATH=~/Desktop` and `outputDirectory=workspace`, the output will be saved to `~/Desktop/workspace/`. (optional) 359 | - `outputFile`: Path to save the output file (optional, auto-generated if not provided) 360 | - `asyncMode`: 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. (optional) 361 | 362 | ### Generate Video 363 | 364 | Generate videos based on text prompts. 365 | 366 | Tool Name: `generate_video` 367 | 368 | Parameters: 369 | - `prompt`: Video description (required) 370 | - `model`: Model version, options are 'T2V-01', 'T2V-01-Director', 'I2V-01', 'I2V-01-Director', 'I2V-01-live', 'S2V-01', 'MiniMax-Hailuo-02', default is 'MiniMax-Hailuo-02' 371 | - `firstFrameImage`: Path to first frame image (optional) 372 | - `duration`: The duration of the video. The model must be "MiniMax-Hailuo-02". Values can be 6 and 10. (optional) 373 | - `resolution`: The resolution of the video. The model must be "MiniMax-Hailuo-02". Values range ["768P", "1080P"]. (optional) 374 | - `outputDirectory`: Directory to save the output file. `outputDirectory` is relative to `MINIMAX_MCP_BASE_PATH` (or `basePath` in config). The final save path is `${basePath}/${outputDirectory}`. For example, if `MINIMAX_MCP_BASE_PATH=~/Desktop` and `outputDirectory=workspace`, the output will be saved to `~/Desktop/workspace/`. (optional) 375 | - `outputFile`: Path to save the output file (optional, auto-generated if not provided) 376 | - `asyncMode`: 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. (optional) 377 | 378 | ### Query Video Generation Status 379 | 380 | Query the status of a video generation task. 381 | 382 | Tool Name: `query_video_generation` 383 | 384 | Parameters: 385 | - `taskId`: The Task ID to query. Should be the task_id returned by `generate_video` tool if `async_mode` is True. (required) 386 | - `outputDirectory`: Directory to save the output file. `outputDirectory` is relative to `MINIMAX_MCP_BASE_PATH` (or `basePath` in config). The final save path is `${basePath}/${outputDirectory}`. For example, if `MINIMAX_MCP_BASE_PATH=~/Desktop` and `outputDirectory=workspace`, the output will be saved to `~/Desktop/workspace/`. (optional) 387 | 388 | ### Generate Music 389 | 390 | Generate music from prompt and lyrics. 391 | 392 | Tool Name: `music_generation` 393 | 394 | Parameters: 395 | - `prompt`: Music creation inspiration describing style, mood, scene, etc. Example: "Pop music, sad, suitable for rainy nights". Character range: [10, 300]. (required) 396 | - `lyrics`: Song lyrics for music generation. Use newline (\\n) to separate each line of lyrics. Supports lyric structure tags [Intro] [Verse] [Chorus] [Bridge] [Outro] to enhance musicality. Character range: [10, 600] (each Chinese character, punctuation, and letter counts as 1 character). (required) 397 | - `sampleRate`: Sample rate of generated music. Values: [16000, 24000, 32000, 44100], default is 32000. (optional) 398 | - `bitrate`: Bitrate of generated music. Values: [32000, 64000, 128000, 256000], default is 128000. (optional) 399 | - `format`: Format of generated music. Values: ["mp3", "wav", "pcm"], default is 'mp3'. (optional) 400 | - `outputDirectory`: The directory to save the output file. `outputDirectory` is relative to `MINIMAX_MCP_BASE_PATH` (or `basePath` in config). The final save path is `${basePath}/${outputDirectory}`. For example, if `MINIMAX_MCP_BASE_PATH=~/Desktop` and `outputDirectory=workspace`, the output will be saved to `~/Desktop/workspace/`. (optional) 401 | 402 | 403 | ### Voice Design 404 | 405 | Generate a voice based on description prompts. 406 | 407 | Tool Name: `voice_design` 408 | 409 | Parameters: 410 | - `prompt`: The prompt to generate the voice from. (required) 411 | - `previewText`: The text to preview the voice. (required) 412 | - `voiceId`: The id of the voice to use. For example, "male-qn-qingse"/"audiobook_female_1"/"cute_boy"/"Charming_Lady"... (optional) 413 | - `outputDirectory`: The directory to save the output file. `outputDirectory` is relative to `MINIMAX_MCP_BASE_PATH` (or `basePath` in config). The final save path is `${basePath}/${outputDirectory}`. For example, if `MINIMAX_MCP_BASE_PATH=~/Desktop` and `outputDirectory=workspace`, the output will be saved to `~/Desktop/workspace/`. (optional) 414 | 415 | ## FAQ 416 | 417 | ### 1. How to use `generate_video` in async-mode 418 | Define completion rules before starting: 419 | 420 | Alternatively, these rules can be configured in your IDE settings (e.g., Cursor): 421 | 422 | 423 | ## Development 424 | 425 | ### Setup 426 | 427 | ```bash 428 | # Clone the repository 429 | git clone https://github.com/MiniMax-AI/MiniMax-MCP-JS.git 430 | cd minimax-mcp-js 431 | 432 | # Install dependencies 433 | pnpm install 434 | ``` 435 | 436 | ### Build 437 | 438 | ```bash 439 | # Build the project 440 | pnpm run build 441 | ``` 442 | 443 | ### Run 444 | 445 | ```bash 446 | # Run the MCP server 447 | pnpm start 448 | ``` 449 | 450 | ## License 451 | 452 | MIT 453 | -------------------------------------------------------------------------------- /src/mcp-rest-server.ts: -------------------------------------------------------------------------------- 1 | import { Server } from '@modelcontextprotocol/sdk/server/index.js'; 2 | import { RestServerTransport } from '@chatmcp/sdk/server/rest.js'; 3 | import { Config } from './types/index.js'; 4 | import { 5 | DEFAULT_SERVER_ENDPOINT, 6 | DEFAULT_SERVER_PORT, 7 | ERROR_API_KEY_REQUIRED, 8 | ERROR_API_HOST_REQUIRED, 9 | RESOURCE_MODE_URL, 10 | TRANSPORT_MODE_REST, 11 | OUTPUT_DIRECTORY_DESCRIPTION, 12 | } from './const/index.js'; 13 | import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; 14 | import { 15 | ListPromptsRequestSchema, 16 | GetPromptRequestSchema, 17 | ListToolsRequestSchema, 18 | CallToolRequestSchema, 19 | ListResourcesRequestSchema, 20 | ReadResourceRequestSchema 21 | } from '@modelcontextprotocol/sdk/types.js'; 22 | import { z } from 'zod'; 23 | import { ServiceManager } from './services/index.js'; 24 | import { MediaService } from './services/media-service.js'; 25 | import { MiniMaxAPI } from './utils/api.js'; 26 | import { ConfigManager } from './config/ConfigManager.js'; 27 | import { COMMON_REST_ARGUMENTS, COMMON_REST_INPUT_SCHEMA_PROPERTIES } from './schema/index.js'; 28 | 29 | // Retry configuration 30 | const MAX_RETRY_ATTEMPTS = 3; 31 | const RETRY_DELAY = 1000; 32 | 33 | /** 34 | * MCP REST Server class, provides REST API implementation of MCPServer functionality 35 | */ 36 | export class MCPRestServer { 37 | private server!: Server; 38 | private mcpServer!: McpServer; 39 | private config!: Config; 40 | private api!: MiniMaxAPI; 41 | private mediaService!: MediaService; 42 | private transport: RestServerTransport | null = null; 43 | 44 | /** 45 | * Create a REST server instance 46 | * @param customConfig Configuration object 47 | */ 48 | constructor(customConfig?: Partial) { 49 | // Initialize configuration 50 | this.initializeConfig(customConfig); 51 | 52 | // Create API and service instances 53 | this.api = new MiniMaxAPI(this.config); 54 | this.mediaService = new MediaService(this.api); 55 | 56 | // Create MCP server instance - used to get tool definitions 57 | this.mcpServer = new McpServer({ 58 | name: 'minimax-mcp-js', 59 | version: '1.0.0', 60 | }); 61 | 62 | // Create low-level server instance 63 | this.server = new Server( 64 | { 65 | name: 'minimax-mcp-js', 66 | version: '1.0.0' 67 | }, 68 | { 69 | capabilities: { 70 | tools: {}, 71 | resources: {}, 72 | prompts: {} 73 | } 74 | } 75 | ); 76 | 77 | // Register request handlers 78 | this.registerRequestHandlers(); 79 | } 80 | 81 | /** 82 | * Initialize configuration 83 | * @param customConfig Custom configuration 84 | */ 85 | private initializeConfig(customConfig?: Partial): void { 86 | // Use ConfigManager to get configuration, automatically handling priorities 87 | this.config = ConfigManager.getConfig(customConfig); 88 | 89 | // Ensure REST transport mode is used 90 | if (this.config.server) { 91 | this.config.server.mode = TRANSPORT_MODE_REST; 92 | } else { 93 | this.config.server = { 94 | mode: TRANSPORT_MODE_REST, 95 | port: DEFAULT_SERVER_PORT, 96 | endpoint: DEFAULT_SERVER_ENDPOINT 97 | }; 98 | } 99 | 100 | // console.log(`[${new Date().toISOString()}] REST server configuration initialized`); 101 | } 102 | 103 | /** 104 | * Update configuration and recreate API instance 105 | * @param newConfig New configuration object 106 | */ 107 | public updateConfig(newConfig: Partial): void { 108 | // Use ConfigManager to merge configurations 109 | this.config = ConfigManager.getConfig(newConfig, this.config); 110 | 111 | // Ensure REST transport mode is used 112 | if (this.config.server) { 113 | this.config.server.mode = TRANSPORT_MODE_REST; 114 | } 115 | 116 | // Update API instance and services 117 | this.api = new MiniMaxAPI(this.config); 118 | this.mediaService = new MediaService(this.api); 119 | 120 | // console.log(`[${new Date().toISOString()}] REST server configuration updated`); 121 | } 122 | 123 | /** 124 | * Register all request handlers 125 | */ 126 | private registerRequestHandlers(): void { 127 | // Tool-related handlers 128 | this.registerToolHandlers(); 129 | 130 | // Resource-related handlers 131 | this.registerResourceHandlers(); 132 | 133 | // Prompt-related handlers 134 | this.registerPromptHandlers(); 135 | } 136 | 137 | /** 138 | * Extract API key from request 139 | * Only get API key from meta.auth 140 | */ 141 | private extractApiKeyFromRequest(request: any): string | undefined { 142 | // Only get from meta.auth 143 | const metaAuth = request.params?._meta?.auth || request.params?.meta?.auth; 144 | if (metaAuth?.api_key) return metaAuth.api_key; 145 | if (metaAuth?.apiKey) return metaAuth.apiKey; 146 | 147 | // API key not found 148 | return undefined; 149 | } 150 | 151 | /** 152 | * Extract configuration from request 153 | * Only supports getting configuration from params.meta.auth 154 | */ 155 | private extractRequestConfig(request: any): Partial { 156 | const config: Partial = {}; 157 | 158 | // Get configuration from params._meta.auth or meta.auth 159 | try { 160 | // First try to get the _meta.auth field from the MCP protocol 161 | let metaAuth = request.params?._meta?.auth; 162 | 163 | // If it doesn't exist, try to get the meta.auth field 164 | if (!metaAuth && request.params?.meta?.auth) { 165 | metaAuth = request.params.meta.auth; 166 | } 167 | 168 | if (metaAuth && typeof metaAuth === 'object') { 169 | // console.log(`[${new Date().toISOString()}] Getting configuration from request meta.auth`); 170 | 171 | // Use ConfigManager to extract configuration 172 | const metaAuthConfig = ConfigManager.extractConfigFromMetaAuth(metaAuth); 173 | if (metaAuthConfig) { 174 | Object.assign(config, metaAuthConfig); 175 | } 176 | } 177 | } catch (error) { 178 | // console.warn(`[${new Date().toISOString()}] Failed to extract configuration from meta.auth:`, error); 179 | } 180 | 181 | return config; 182 | } 183 | 184 | /** 185 | * Create configuration for request 186 | */ 187 | private getRequestConfig(request: any): Config { 188 | // Get request-specific configuration 189 | const requestConfig = this.extractRequestConfig(request); 190 | 191 | // Merge configurations, request configuration has highest priority 192 | return ConfigManager.getConfig(requestConfig, this.config); 193 | } 194 | 195 | /** 196 | * Register tool-related request handlers 197 | */ 198 | private registerToolHandlers(): void { 199 | // List tools handler 200 | this.server.setRequestHandler(ListToolsRequestSchema, async (request) => { 201 | try { 202 | return { 203 | tools: [ 204 | { 205 | name: 'text_to_audio', 206 | description: 'Convert text to audio', 207 | arguments: [ 208 | { name: 'text', description: 'Text to convert to audio', required: true }, 209 | { name: 'outputDirectory', description: OUTPUT_DIRECTORY_DESCRIPTION, required: false }, 210 | { name: 'voiceId', description: 'Voice ID to use, e.g. "female-shaonv"', required: false }, 211 | { name: 'model', description: 'Model to use', required: false }, 212 | { name: 'speed', description: 'Speech speed (0.5-2.0)', required: false }, 213 | { name: 'vol', description: 'Speech volume (0.1-10.0)', required: false }, 214 | { name: 'pitch', description: 'Speech pitch (-12 to 12)', required: false }, 215 | { name: 'emotion', description: 'Speech emotion, values: ["happy", "sad", "angry", "fearful", "disgusted", "surprised", "neutral"]', required: false }, 216 | { name: 'format', description: 'Audio format, values: ["pcm", "mp3","flac", "wav"]', required: false }, 217 | { name: 'sampleRate', description: 'Sample rate (Hz), values: [8000, 16000, 22050, 24000, 32000, 44100]', required: false }, 218 | { name: 'bitrate', description: 'Bitrate (bps), values: [64000, 96000, 128000, 160000, 192000, 224000, 256000, 320000]', required: false }, 219 | { name: 'channel', description: 'Audio channels, values: [1, 2]', required: false }, 220 | { name: 'languageBoost', description: `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'`, required: false }, 221 | { name: 'subtitleEnable', description: `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`, required: false }, 222 | { name: 'outputFile', description: 'Output file path, auto-generated if not provided', required: false } 223 | ], 224 | inputSchema: { 225 | type: 'object', 226 | properties: { 227 | text: { type: 'string' }, 228 | outputDirectory: { type: 'string' }, 229 | voiceId: { type: 'string' }, 230 | model: { type: 'string' }, 231 | speed: { type: 'number' }, 232 | vol: { type: 'number' }, 233 | pitch: { type: 'number' }, 234 | emotion: { type: 'string' }, 235 | format: { type: 'string' }, 236 | sampleRate: { type: 'number' }, 237 | bitrate: { type: 'number' }, 238 | channel: { type: 'number' }, 239 | languageBoost: { type: 'string' }, 240 | subtitleEnable: { type: 'boolean' }, 241 | outputFile: { type: 'string' } 242 | }, 243 | required: ['text'] 244 | } 245 | }, 246 | { 247 | name: 'list_voices', 248 | description: 'List all available voices', 249 | arguments: [ 250 | { name: 'voiceType', description: 'Type of voices to list, values: ["all", "system", "voice_cloning"]', required: false } 251 | ], 252 | inputSchema: { 253 | type: 'object', 254 | properties: { 255 | voiceType: { type: 'string' } 256 | }, 257 | required: ['voiceType'] 258 | } 259 | }, 260 | { 261 | name: 'play_audio', 262 | description: 'Play audio file. Supports WAV and MP3 formats. Does not support video.', 263 | arguments: [ 264 | { name: 'inputFilePath', description: 'Path to audio file to play', required: true }, 265 | { name: 'isUrl', description: 'Whether audio file is a URL', required: false } 266 | ], 267 | inputSchema: { 268 | type: 'object', 269 | properties: { 270 | inputFilePath: { type: 'string' }, 271 | isUrl: { type: 'boolean' } 272 | }, 273 | required: ['inputFilePath'] 274 | } 275 | }, 276 | { 277 | name: 'text_to_image', 278 | description: 'Generate image based on text prompt', 279 | arguments: [ 280 | { name: 'prompt', description: 'Text prompt for image generation', required: true }, 281 | { name: 'model', description: 'Model to use', required: false }, 282 | { name: 'aspectRatio', description: 'Image aspect ratio, values: ["1:1", "16:9","4:3", "3:2", "2:3", "3:4", "9:16", "21:9"]', required: false }, 283 | { name: 'n', description: 'Number of images to generate (1-9)', required: false }, 284 | { name: 'promptOptimizer', description: 'Whether to optimize prompt', required: false }, 285 | { name: 'outputDirectory', description: OUTPUT_DIRECTORY_DESCRIPTION, required: false }, 286 | { name: 'outputFile', description: 'Output file path, auto-generated if not provided', required: false } 287 | ], 288 | inputSchema: { 289 | type: 'object', 290 | properties: { 291 | prompt: { type: 'string' }, 292 | model: { type: 'string' }, 293 | aspectRatio: { type: 'string' }, 294 | n: { type: 'number' }, 295 | promptOptimizer: { type: 'boolean' }, 296 | outputDirectory: { type: 'string' }, 297 | outputFile: { type: 'string' } 298 | }, 299 | required: ['prompt'] 300 | } 301 | }, 302 | { 303 | name: 'generate_video', 304 | description: 'Generate video based on text prompt', 305 | arguments: [ 306 | { name: 'prompt', description: 'Text prompt for video generation', required: true }, 307 | { name: 'model', description: 'Model to use, values: ["T2V-01", "T2V-01-Director", "I2V-01", "I2V-01-Director", "I2V-01-live", "MiniMax-Hailuo-02"]', required: false }, 308 | { name: 'firstFrameImage', description: 'First frame image', required: false }, 309 | { name: 'outputDirectory', description: OUTPUT_DIRECTORY_DESCRIPTION, required: false }, 310 | { name: 'outputFile', description: 'Output file path, auto-generated if not provided', required: false }, 311 | { name: 'async_mode', description: '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', required: false }, 312 | { name: 'resolution', description: 'The resolution of the video. The model must be "MiniMax-Hailuo-02". Values range ["768P", "1080P"]', required: false }, 313 | { name: 'duration', description: 'The duration of the video. The model must be "MiniMax-Hailuo-02". Values can be 6 and 10.', required: false }, 314 | ], 315 | inputSchema: { 316 | type: 'object', 317 | properties: { 318 | prompt: { type: 'string' }, 319 | model: { type: 'string' }, 320 | firstFrameImage: { type: 'string' }, 321 | outputDirectory: { type: 'string' }, 322 | outputFile: { type: 'string' }, 323 | async_mode: { type: 'boolean' }, 324 | resolution: { type: 'string' }, 325 | duration: { type: 'number' } 326 | }, 327 | required: ['prompt'] 328 | } 329 | }, 330 | { 331 | name: 'voice_clone', 332 | description: 'Clone voice using provided audio file', 333 | arguments: [ 334 | { name: 'voiceId', description: 'Voice ID to use', required: true }, 335 | { name: 'audioFile', description: 'Audio file path', required: true }, 336 | { name: 'text', description: 'Text for demo audio', required: false }, 337 | { name: 'outputDirectory', description: OUTPUT_DIRECTORY_DESCRIPTION, required: false }, 338 | { name: 'isUrl', description: 'Whether audio file is a URL', required: false } 339 | ], 340 | inputSchema: { 341 | type: 'object', 342 | properties: { 343 | voiceId: { type: 'string' }, 344 | audioFile: { type: 'string' }, 345 | text: { type: 'string' }, 346 | outputDirectory: { type: 'string' }, 347 | isUrl: { type: 'boolean' } 348 | }, 349 | required: ['voiceId', 'audioFile'] 350 | } 351 | }, 352 | { 353 | name: 'image_to_video', 354 | description: 'Generate video based on image', 355 | arguments: [ 356 | { name: 'prompt', description: 'Text prompt for video generation', required: true }, 357 | { name: 'firstFrameImage', description: 'Path to first frame image', required: true }, 358 | { name: 'model', description: 'Model to use, values: ["I2V-01", "I2V-01-Director", "I2V-01-live"]', required: false }, 359 | { name: 'outputDirectory', description: OUTPUT_DIRECTORY_DESCRIPTION, required: false }, 360 | { name: 'outputFile', description: 'Output file path, auto-generated if not provided', required: false }, 361 | { name: 'async_mode', description: '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', required: false } 362 | ], 363 | inputSchema: { 364 | type: 'object', 365 | properties: { 366 | prompt: { type: 'string' }, 367 | firstFrameImage: { type: 'string' }, 368 | model: { type: 'string' }, 369 | outputDirectory: { type: 'string' }, 370 | outputFile: { type: 'string' }, 371 | async_mode: { type: 'boolean' } 372 | }, 373 | required: ['prompt', 'firstFrameImage'] 374 | } 375 | }, 376 | { 377 | name: 'music_generation', 378 | description: 'Generate music based on text prompt and lyrics', 379 | arguments: [ 380 | { name: 'prompt', description: 'Music creation inspiration describing style, mood, scene, etc.', required: true }, 381 | { name: 'lyrics', description: '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)', required: true }, 382 | { name: 'sampleRate', description: 'Sample rate of generated music', required: false }, 383 | { name: 'bitrate', description: 'Bitrate of generated music', required: false }, 384 | { name: 'format', description: 'Format of generated music', required: false }, 385 | { name: 'outputDirectory', description: OUTPUT_DIRECTORY_DESCRIPTION, required: false } 386 | ], 387 | inputSchema: { 388 | type: 'object', 389 | properties: { 390 | prompt: { type: 'string' }, 391 | lyrics: { type: 'string' }, 392 | sampleRate: { type: 'number' }, 393 | bitrate: { type: 'number' }, 394 | format: { type: 'string' }, 395 | outputDirectory: { type: 'string' } 396 | }, 397 | required: ['prompt', 'lyrics'] 398 | } 399 | }, 400 | { 401 | name: 'voice_design', 402 | description: 'Generate a voice based on description prompts', 403 | arguments: [ 404 | { name: 'prompt', description: 'The prompt to generate the voice from', required: true }, 405 | { name: 'previewText', description: 'The text to preview the voice', required: true }, 406 | { name: 'voiceId', description: 'The id of the voice to use', required: false }, 407 | { name: 'outputDirectory', description: OUTPUT_DIRECTORY_DESCRIPTION, required: false } 408 | ], 409 | inputSchema: { 410 | type: 'object', 411 | properties: { 412 | prompt: { type: 'string' }, 413 | previewText: { type: 'string' }, 414 | voiceId: { type: 'string' }, 415 | outputDirectory: { type: 'string' } 416 | }, 417 | required: ['prompt', 'previewText'] 418 | } 419 | } 420 | ] 421 | }; 422 | } catch (error) { 423 | throw this.wrapError('Failed to get tool list', error); 424 | } 425 | }); 426 | 427 | // Call tool handler 428 | this.server.setRequestHandler(CallToolRequestSchema, async (request) => { 429 | const toolName = request.params.tool; 430 | const toolParams = request.params.params || {}; 431 | 432 | try { 433 | // Create configuration and API instance for this request 434 | const requestConfig = this.getRequestConfig(request); 435 | const requestApi = new MiniMaxAPI(requestConfig); 436 | const mediaService = new MediaService(requestApi); 437 | 438 | // Log API key (partially hidden) 439 | const apiKey = this.extractApiKeyFromRequest(request); 440 | const maskedKey = apiKey 441 | ? `${apiKey.substring(0, 4)}****${apiKey.substring(apiKey.length - 4)}` 442 | : 'not provided'; 443 | // console.log(`[${new Date().toISOString()}] Using API key: ${maskedKey} to call tool: ${toolName}`); 444 | 445 | // Choose different handler function based on tool name 446 | switch (toolName) { 447 | case 'text_to_audio': 448 | return await this.handleTextToAudio(toolParams, requestApi, mediaService); 449 | 450 | case 'list_voices': 451 | return await this.handleListVoices(toolParams, requestApi, mediaService); 452 | 453 | case 'play_audio': 454 | return await this.handlePlayAudio(toolParams); 455 | 456 | case 'text_to_image': 457 | return await this.handleTextToImage(toolParams, requestApi, mediaService); 458 | 459 | case 'generate_video': 460 | return await this.handleGenerateVideo(toolParams, requestApi, mediaService); 461 | 462 | case 'voice_clone': 463 | return await this.handleVoiceClone(toolParams, requestApi, mediaService); 464 | 465 | case 'image_to_video': 466 | return await this.handleImageToVideo(toolParams, requestApi, mediaService); 467 | 468 | case 'query_video_generation': 469 | return await this.handleVideoGenerationQuery(toolParams, requestApi, mediaService); 470 | 471 | case 'music_generation': 472 | return await this.handleGenerateMusic(toolParams, requestApi, mediaService); 473 | 474 | case 'voice_design': 475 | return await this.handleVoiceDesign(toolParams, requestApi, mediaService); 476 | 477 | default: 478 | throw new Error(`Unknown tool: ${toolName}`); 479 | } 480 | } catch (error) { 481 | throw this.wrapError(`Failed to call tool ${toolName}`, error); 482 | } 483 | }); 484 | } 485 | 486 | /** 487 | * Handle text to speech request 488 | */ 489 | private async handleTextToAudio(args: any, api: MiniMaxAPI, mediaService: MediaService, attempt = 1): Promise { 490 | try { 491 | // Call media service to handle request 492 | const result = await mediaService.generateSpeech(args); 493 | return result; 494 | } catch (error) { 495 | if (attempt < MAX_RETRY_ATTEMPTS) { 496 | // console.warn(`[${new Date().toISOString()}] Failed to generate speech, attempting retry (${attempt}/${MAX_RETRY_ATTEMPTS})`, error); 497 | // Delay retry 498 | await new Promise(resolve => setTimeout(resolve, RETRY_DELAY * Math.pow(2, attempt - 1))); 499 | return this.handleTextToAudio(args, api, mediaService, attempt + 1); 500 | } 501 | throw this.wrapError('Failed to generate speech', error); 502 | } 503 | } 504 | 505 | /** 506 | * Handle list voices request 507 | */ 508 | private async handleListVoices(args: any, api: MiniMaxAPI, mediaService: MediaService, attempt = 1): Promise { 509 | try { 510 | // Call media service to handle request 511 | const result = await mediaService.listVoices(args); 512 | return result; 513 | } catch (error) { 514 | if (attempt < MAX_RETRY_ATTEMPTS) { 515 | // console.warn(`[${new Date().toISOString()}] Failed to list voices, attempting retry (${attempt}/${MAX_RETRY_ATTEMPTS})`, error); 516 | // Delay retry 517 | await new Promise(resolve => setTimeout(resolve, RETRY_DELAY * Math.pow(2, attempt - 1))); 518 | return this.handleListVoices(args, api, mediaService, attempt + 1); 519 | } 520 | throw this.wrapError('Failed to list voices', error); 521 | } 522 | } 523 | 524 | /** 525 | * Handle play audio request 526 | */ 527 | private async handlePlayAudio(args: any): Promise { 528 | try { 529 | // This operation needs to use the current mediaService instance 530 | return { 531 | content: [ 532 | { 533 | type: 'text', 534 | text: `Playing audio: ${args.inputFilePath}`, 535 | }, 536 | ], 537 | }; 538 | } catch (error) { 539 | throw this.wrapError('Failed to play audio', error); 540 | } 541 | } 542 | 543 | /** 544 | * Handle text to image request 545 | */ 546 | private async handleTextToImage(args: any, api: MiniMaxAPI, mediaService: MediaService, attempt = 1): Promise { 547 | try { 548 | // Call media service to handle request 549 | const result = await mediaService.generateImage(args); 550 | return result; 551 | } catch (error) { 552 | if (attempt < MAX_RETRY_ATTEMPTS) { 553 | // console.warn(`[${new Date().toISOString()}] Failed to generate image, attempting retry (${attempt}/${MAX_RETRY_ATTEMPTS})`, error); 554 | // Delay retry 555 | await new Promise(resolve => setTimeout(resolve, RETRY_DELAY * Math.pow(2, attempt - 1))); 556 | return this.handleTextToImage(args, api, mediaService, attempt + 1); 557 | } 558 | throw this.wrapError('Failed to generate image', error); 559 | } 560 | } 561 | 562 | /** 563 | * Handle generate video request 564 | */ 565 | private async handleGenerateVideo(args: any, api: MiniMaxAPI, mediaService: MediaService, attempt = 1): Promise { 566 | try { 567 | // Call media service to handle request 568 | const result = await mediaService.generateVideo(args); 569 | return result; 570 | } catch (error) { 571 | if (attempt < MAX_RETRY_ATTEMPTS) { 572 | // console.warn(`[${new Date().toISOString()}] Failed to generate video, attempting retry (${attempt}/${MAX_RETRY_ATTEMPTS})`, error); 573 | // Delay retry 574 | await new Promise(resolve => setTimeout(resolve, RETRY_DELAY * Math.pow(2, attempt - 1))); 575 | return this.handleGenerateVideo(args, api, mediaService, attempt + 1); 576 | } 577 | throw this.wrapError('Failed to generate video', error); 578 | } 579 | } 580 | 581 | /** 582 | * Handle voice clone request 583 | */ 584 | private async handleVoiceClone(args: any, api: MiniMaxAPI, mediaService: MediaService, attempt = 1): Promise { 585 | try { 586 | // Call media service to handle request 587 | const result = await mediaService.cloneVoice(args); 588 | return result; 589 | } catch (error) { 590 | // Check if this is a real-name verification error 591 | const errorMessage = error instanceof Error ? error.message : String(error); 592 | if (errorMessage.includes('voice clone user forbidden') || 593 | errorMessage.includes('should complete real-name verification')) { 594 | 595 | // Domestic platform verification URL 596 | const verificationUrl = 'https://platform.minimaxi.com/user-center/basic-information'; 597 | 598 | return { 599 | content: [ 600 | { 601 | type: 'text', 602 | 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.`, 603 | }, 604 | ], 605 | }; 606 | } 607 | 608 | // Regular retry mechanism 609 | if (attempt < MAX_RETRY_ATTEMPTS) { 610 | // console.warn(`[${new Date().toISOString()}] Failed to clone voice, attempting retry (${attempt}/${MAX_RETRY_ATTEMPTS})`, error); 611 | // Delay retry 612 | await new Promise(resolve => setTimeout(resolve, RETRY_DELAY * Math.pow(2, attempt - 1))); 613 | return this.handleVoiceClone(args, api, mediaService, attempt + 1); 614 | } 615 | throw this.wrapError('Failed to clone voice', error); 616 | } 617 | } 618 | 619 | /** 620 | * Handle image to video request 621 | */ 622 | private async handleImageToVideo(args: any, api: MiniMaxAPI, mediaService: MediaService, attempt = 1): Promise { 623 | try { 624 | // Ensure model is suitable for image to video conversion 625 | if (!args.model) { 626 | args.model = 'I2V-01'; 627 | } 628 | 629 | // Ensure firstFrameImage parameter exists 630 | if (!args.firstFrameImage) { 631 | throw new Error('Missing required parameter: firstFrameImage'); 632 | } 633 | 634 | // Auto-generate output filename if not provided 635 | if (!args.outputFile) { 636 | const promptPrefix = args.prompt.substring(0, 20).replace(/[^\w]/g, '_'); 637 | args.outputFile = `i2v_${promptPrefix}_${Date.now()}`; 638 | } 639 | 640 | // Call media service to handle request 641 | const result = await mediaService.generateVideo(args); 642 | return result; 643 | } catch (error) { 644 | if (attempt < MAX_RETRY_ATTEMPTS) { 645 | // console.warn(`[${new Date().toISOString()}] Failed to generate video, attempting retry (${attempt}/${MAX_RETRY_ATTEMPTS})`, error); 646 | // Delay retry 647 | await new Promise(resolve => setTimeout(resolve, RETRY_DELAY * Math.pow(2, attempt - 1))); 648 | return this.handleImageToVideo(args, api, mediaService, attempt + 1); 649 | } 650 | throw this.wrapError('Failed to generate video', error); 651 | } 652 | } 653 | 654 | /** 655 | * Handle video generation query request 656 | */ 657 | private async handleVideoGenerationQuery(args: any, api: MiniMaxAPI, mediaService: MediaService, attempt = 1): Promise { 658 | try { 659 | // Call media service to handle request 660 | const result = await mediaService.queryVideoGeneration(args); 661 | return result; 662 | } catch (error) { 663 | throw this.wrapError('Failed to query video generation', error); 664 | } 665 | } 666 | 667 | /** 668 | * Handle generate music request 669 | */ 670 | private async handleGenerateMusic(args: any, api: MiniMaxAPI, mediaService: MediaService, attempt = 1): Promise { 671 | try { 672 | // Call media service to handle request 673 | const result = await mediaService.generateMusic(args); 674 | return result; 675 | } catch (error) { 676 | throw this.wrapError('Failed to generate music', error); 677 | } 678 | } 679 | 680 | /** 681 | * Handle voice design request 682 | */ 683 | private async handleVoiceDesign(args: any, api: MiniMaxAPI, mediaService: MediaService, attempt = 1): Promise { 684 | try { 685 | // Call media service to handle request 686 | const result = await mediaService.designVoice(args); 687 | return result; 688 | } catch (error) { 689 | throw this.wrapError('Failed to design voice', error); 690 | } 691 | } 692 | 693 | /** 694 | * Register resource-related request handlers 695 | */ 696 | private registerResourceHandlers(): void { 697 | // List resources handler 698 | this.server.setRequestHandler(ListResourcesRequestSchema, async (request) => { 699 | try { 700 | // Create configuration for this request 701 | const requestConfig = this.getRequestConfig(request); 702 | 703 | // Return empty list - our implementation doesn't support resource listing 704 | return { 705 | resources: [] 706 | }; 707 | } catch (error) { 708 | throw this.wrapError('Failed to get resource list', error); 709 | } 710 | }); 711 | 712 | // Read resource handler 713 | this.server.setRequestHandler(ReadResourceRequestSchema, async (request) => { 714 | try { 715 | // Create configuration for this request 716 | const requestConfig = this.getRequestConfig(request); 717 | 718 | // Our implementation doesn't support resource reading, so always return 404 719 | throw new Error(`Resource does not exist: ${request.params.uri}`); 720 | } catch (error) { 721 | throw this.wrapError('Failed to read resource', error); 722 | } 723 | }); 724 | } 725 | 726 | /** 727 | * Register prompt-related request handlers 728 | */ 729 | private registerPromptHandlers(): void { 730 | // List prompts handler 731 | this.server.setRequestHandler(ListPromptsRequestSchema, async (request) => { 732 | try { 733 | // Create configuration for this request 734 | const requestConfig = this.getRequestConfig(request); 735 | 736 | // Return empty list - our implementation doesn't support prompt listing 737 | return { 738 | prompts: [] 739 | }; 740 | } catch (error) { 741 | throw this.wrapError('Failed to get prompt list', error); 742 | } 743 | }); 744 | 745 | // Get prompt handler 746 | this.server.setRequestHandler(GetPromptRequestSchema, async (request) => { 747 | try { 748 | // Create configuration for this request 749 | const requestConfig = this.getRequestConfig(request); 750 | 751 | // Our implementation doesn't support prompt retrieval, so always return 404 752 | throw new Error(`Prompt does not exist: ${request.params.name}`); 753 | } catch (error) { 754 | throw this.wrapError('Failed to get prompt', error); 755 | } 756 | }); 757 | } 758 | 759 | /** 760 | * Start REST server 761 | */ 762 | public async start(): Promise { 763 | try { 764 | const port = this.config.server?.port || DEFAULT_SERVER_PORT; 765 | const endpoint = this.config.server?.endpoint || DEFAULT_SERVER_ENDPOINT; 766 | 767 | // Create transport instance 768 | this.transport = new RestServerTransport({ 769 | endpoint: endpoint, 770 | port: port 771 | }); 772 | 773 | // Connect server 774 | await this.server.connect(this.transport); 775 | 776 | // Start HTTP server 777 | await this.transport.startServer(); 778 | 779 | // console.log(`[${new Date().toISOString()}] MiniMax MCP REST server started at: http://localhost:${port}${endpoint}`); 780 | } catch (error) { 781 | // console.error(`[${new Date().toISOString()}] Failed to start REST server:`, error); 782 | throw error; 783 | } 784 | } 785 | 786 | /** 787 | * Stop REST server 788 | */ 789 | public async stop(): Promise { 790 | try { 791 | if (this.transport) { 792 | await this.transport.close(); 793 | this.transport = null; 794 | // console.log(`[${new Date().toISOString()}] REST server stopped`); 795 | } 796 | } catch (error) { 797 | // console.error(`[${new Date().toISOString()}] Failed to stop REST server:`, error); 798 | throw error; 799 | } 800 | } 801 | 802 | /** 803 | * Get MCP server instance 804 | */ 805 | public getMCPServer(): McpServer { 806 | return this.mcpServer; 807 | } 808 | 809 | /** 810 | * Get server instance 811 | */ 812 | public getServer(): Server { 813 | return this.server; 814 | } 815 | 816 | /** 817 | * Get current configuration 818 | */ 819 | public getConfig(): Config { 820 | return this.config; 821 | } 822 | 823 | /** 824 | * Wrap error message 825 | * @param message Error message prefix 826 | * @param error Original error 827 | * @returns Wrapped error 828 | */ 829 | private wrapError(message: string, error: unknown): Error { 830 | if (error instanceof Error) { 831 | const wrappedError = new Error(`${message}: ${error.message}`); 832 | wrappedError.stack = error.stack; 833 | return wrappedError; 834 | } 835 | return new Error(`${message}: ${String(error)}`); 836 | } 837 | } 838 | -------------------------------------------------------------------------------- /src/mcp-server.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; 4 | import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; 5 | import { z } from 'zod'; 6 | import { Config, ServerOptions, TransportMode } from './types/index.js'; 7 | import { MiniMaxAPI } from './utils/api.js'; 8 | import { TTSAPI } from './api/tts.js'; 9 | import { ImageAPI } from './api/image.js'; 10 | import { VideoAPI } from './api/video.js'; 11 | import { VoiceCloneAPI } from './api/voice-clone.js'; 12 | import { VoiceAPI } from './api/voice.js'; 13 | import { VoiceDesignAPI } from './api/voice-design.js'; 14 | import { MusicAPI } from './api/music.js'; 15 | import { playAudio } from './utils/audio.js'; 16 | import { getParamValue } from '@chatmcp/sdk/utils/index.js'; 17 | import fs from 'fs'; 18 | import { 19 | DEFAULT_API_HOST, 20 | DEFAULT_BITRATE, 21 | DEFAULT_CHANNEL, 22 | DEFAULT_EMOTION, 23 | DEFAULT_FORMAT, 24 | DEFAULT_LANGUAGE_BOOST, 25 | DEFAULT_PITCH, 26 | DEFAULT_SAMPLE_RATE, 27 | DEFAULT_SPEECH_MODEL, 28 | DEFAULT_SPEED, 29 | DEFAULT_T2I_MODEL, 30 | DEFAULT_VIDEO_MODEL, 31 | DEFAULT_VOICE_ID, 32 | DEFAULT_VOLUME, 33 | ENV_CONFIG_PATH, 34 | ENV_MINIMAX_API_HOST, 35 | ENV_MINIMAX_API_KEY, 36 | ENV_MINIMAX_MCP_BASE_PATH, 37 | ENV_RESOURCE_MODE, 38 | ERROR_API_HOST_REQUIRED, 39 | ERROR_API_KEY_REQUIRED, 40 | RESOURCE_MODE_URL, 41 | } from './const/index.js'; 42 | import { ConfigManager } from './config/ConfigManager.js'; 43 | import { COMMON_PARAMETERS_SCHEMA } from './schema/index.js'; 44 | 45 | /** 46 | * MCP Server class for managing MiniMax MCP server configuration and functionality 47 | * Specifically designed for STDIO transport mode 48 | */ 49 | export class MCPServer { 50 | private server: McpServer; 51 | private config!: Config; 52 | private api: MiniMaxAPI; 53 | private ttsApi: TTSAPI; 54 | private imageApi: ImageAPI; 55 | private videoApi: VideoAPI; 56 | private voiceCloneApi: VoiceCloneAPI; 57 | private voiceApi: VoiceAPI; 58 | private voiceDesignApi: VoiceDesignAPI; 59 | private musicApi: MusicAPI; 60 | 61 | /** 62 | * Create an MCP server instance (STDIO mode) 63 | * @param customConfig Optional custom configuration 64 | */ 65 | constructor(customConfig?: Partial) { 66 | // Initialize configuration 67 | this.initializeConfig(customConfig); 68 | 69 | // Create API instances 70 | this.api = new MiniMaxAPI(this.config); 71 | this.ttsApi = new TTSAPI(this.api); 72 | this.imageApi = new ImageAPI(this.api); 73 | this.videoApi = new VideoAPI(this.api); 74 | this.voiceCloneApi = new VoiceCloneAPI(this.api); 75 | this.voiceApi = new VoiceAPI(this.api); 76 | this.voiceDesignApi = new VoiceDesignAPI(this.api); 77 | this.musicApi = new MusicAPI(this.api); 78 | 79 | // Create server instance 80 | this.server = new McpServer({ 81 | name: 'minimax-mcp-js', 82 | version: '1.0.0', 83 | }); 84 | 85 | // Register all tools 86 | this.registerTools(); 87 | } 88 | 89 | /** 90 | * Initialize configuration 91 | * @param customConfig Custom configuration 92 | */ 93 | private initializeConfig(customConfig?: Partial): void { 94 | // Use ConfigManager to get configuration, automatically handling priorities 95 | this.config = ConfigManager.getConfig(customConfig); 96 | 97 | // Remove unnecessary server configuration for STDIO mode 98 | // STDIO mode doesn't need port and endpoint configuration 99 | delete this.config.server; 100 | 101 | // console.log(`[${new Date().toISOString()}] STDIO server configuration initialized`); 102 | } 103 | 104 | /** 105 | * Register all MCP tools 106 | */ 107 | private registerTools(): void { 108 | this.registerTextToAudioTool(); 109 | this.registerListVoicesTool(); 110 | this.registerPlayAudioTool(); 111 | this.registerVoiceCloneTool(); 112 | this.registerTextToImageTool(); 113 | this.registerGenerateVideoTool(); 114 | this.registerImageToVideoTool(); 115 | this.registerQueryVideoGenerationTool(); 116 | this.registerMusicGenerationTool(); 117 | this.registerVoiceDesignTool(); 118 | } 119 | 120 | /** 121 | * Register text-to-speech tool 122 | */ 123 | private registerTextToAudioTool(): void { 124 | this.server.tool( 125 | 'text_to_audio', 126 | '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.', 127 | { 128 | text: z.string().describe('Text to convert to audio'), 129 | outputDirectory: COMMON_PARAMETERS_SCHEMA.outputDirectory, 130 | voiceId: z.string().optional().default(DEFAULT_VOICE_ID).describe('Voice ID to use, e.g. "female-shaonv"'), 131 | model: z.string().optional().default(DEFAULT_SPEECH_MODEL).describe('Model to use'), 132 | speed: z.number().min(0.5).max(2.0).optional().default(DEFAULT_SPEED).describe('Speech speed'), 133 | vol: z.number().min(0.1).max(10.0).optional().default(DEFAULT_VOLUME).describe('Speech volume'), 134 | pitch: z.number().min(-12).max(12).optional().default(DEFAULT_PITCH).describe('Speech pitch'), 135 | emotion: z 136 | .string() 137 | .optional() 138 | .default(DEFAULT_EMOTION) 139 | .describe('Speech emotion, values: ["happy", "sad", "angry", "fearful", "disgusted", "surprised", "neutral"]'), 140 | format: z 141 | .string() 142 | .optional() 143 | .default(DEFAULT_FORMAT) 144 | .describe('Audio format, values: ["pcm", "mp3","flac", "wav"]'), 145 | sampleRate: z 146 | .number() 147 | .optional() 148 | .default(DEFAULT_SAMPLE_RATE) 149 | .describe('Sample rate (Hz), values: [8000, 16000, 22050, 24000, 32000, 44100]'), 150 | bitrate: z 151 | .number() 152 | .optional() 153 | .default(DEFAULT_BITRATE) 154 | .describe('Bitrate (bps), values: [64000, 96000, 128000, 160000, 192000, 224000, 256000, 320000]'), 155 | channel: z.number().optional().default(DEFAULT_CHANNEL).describe('Audio channels, values: [1, 2]'), 156 | languageBoost: z.string().optional().default(DEFAULT_LANGUAGE_BOOST) 157 | .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'`), 158 | subtitleEnable: z 159 | .boolean() 160 | .optional() 161 | .default(false) 162 | .describe( 163 | `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`, 164 | ), 165 | outputFile: z 166 | .string() 167 | .optional() 168 | .describe('Path to save the generated audio file, automatically generated if not provided'), 169 | }, 170 | async (args, extra) => { 171 | try { 172 | // Build TTS request parameters 173 | const ttsParams = { 174 | text: args.text, 175 | outputDirectory: args.outputDirectory, 176 | voiceId: args.voiceId || DEFAULT_VOICE_ID, 177 | model: args.model || DEFAULT_SPEECH_MODEL, 178 | speed: args.speed || DEFAULT_SPEED, 179 | vol: args.vol || DEFAULT_VOLUME, 180 | pitch: args.pitch || DEFAULT_PITCH, 181 | emotion: args.emotion || DEFAULT_EMOTION, 182 | format: args.format || DEFAULT_FORMAT, 183 | sampleRate: args.sampleRate || DEFAULT_SAMPLE_RATE, 184 | bitrate: args.bitrate || DEFAULT_BITRATE, 185 | channel: args.channel || DEFAULT_CHANNEL, 186 | languageBoost: args.languageBoost || DEFAULT_LANGUAGE_BOOST, 187 | subtitleEnable: args.subtitleEnable || false, 188 | outputFile: args.outputFile, 189 | }; 190 | 191 | // Use global configuration 192 | const requestApiKey = this.config.apiKey; 193 | 194 | if (!requestApiKey) { 195 | throw new Error(ERROR_API_KEY_REQUIRED); 196 | } 197 | 198 | // Update configuration with request-specific parameters 199 | const requestConfig: Partial = { 200 | apiKey: requestApiKey, 201 | apiHost: this.config.apiHost, 202 | resourceMode: this.config.resourceMode, 203 | }; 204 | 205 | // Update API instance 206 | const requestApi = new MiniMaxAPI(requestConfig as Config); 207 | const requestTtsApi = new TTSAPI(requestApi); 208 | 209 | // Automatically set resource mode (if not specified) 210 | const outputFormat = requestConfig.resourceMode; 211 | const ttsRequest = { 212 | ...ttsParams, 213 | outputFormat, 214 | }; 215 | 216 | // If no output filename is provided, generate one automatically 217 | if (!ttsRequest.outputFile) { 218 | const textPrefix = ttsRequest.text.substring(0, 20).replace(/[^\w]/g, '_'); 219 | ttsRequest.outputFile = `tts_${textPrefix}_${Date.now()}`; 220 | } 221 | 222 | const result = await requestTtsApi.generateSpeech(ttsRequest); 223 | 224 | // Return different messages based on output format 225 | if (outputFormat === RESOURCE_MODE_URL) { 226 | return { 227 | content: [ 228 | { 229 | type: 'text', 230 | text: `Success. Audio URL: ${result.audio}. ${ttsParams.subtitleEnable ? `Subtitle file saved: ${result.subtitle}` : ''}`, 231 | }, 232 | ], 233 | }; 234 | } else { 235 | return { 236 | content: [ 237 | { 238 | type: 'text', 239 | text: `Audio file saved: ${result.audio}. ${ttsParams.subtitleEnable ? `Subtitle file saved: ${result.subtitle}. ` : ''}Voice used: ${ttsParams.voiceId}`, 240 | }, 241 | ], 242 | }; 243 | } 244 | } catch (error) { 245 | return { 246 | content: [ 247 | { 248 | type: 'text', 249 | text: `Failed to generate audio: ${error instanceof Error ? error.message : String(error)}`, 250 | }, 251 | ], 252 | }; 253 | } 254 | }, 255 | ); 256 | } 257 | 258 | /** 259 | * Register list voices tool 260 | */ 261 | private registerListVoicesTool(): void { 262 | this.server.tool( 263 | 'list_voices', 264 | 'List all available voices. Only supported when api_host is https://api.minimax.chat.', 265 | { 266 | voiceType: z 267 | .string() 268 | .optional() 269 | .default('all') 270 | .describe('Type of voices to list, values: ["all", "system", "voice_cloning"]'), 271 | }, 272 | async (params) => { 273 | try { 274 | // No need to update configuration from request parameters in stdio mode 275 | const result = await this.voiceApi.listVoices(params); 276 | 277 | return { 278 | content: [ 279 | { 280 | type: 'text', 281 | text: `Success. System voices: ${result.systemVoices.join(', ')}, Cloned voices: ${result.voiceCloneVoices.join(', ')}`, 282 | }, 283 | ], 284 | }; 285 | } catch (error) { 286 | return { 287 | content: [ 288 | { 289 | type: 'text', 290 | text: `Failed to list voices: ${error instanceof Error ? error.message : String(error)}`, 291 | }, 292 | ], 293 | }; 294 | } 295 | }, 296 | ); 297 | } 298 | 299 | /** 300 | * Register play audio tool 301 | */ 302 | private registerPlayAudioTool(): void { 303 | this.server.tool( 304 | 'play_audio', 305 | 'Play an audio file. Supports WAV and MP3 formats. Does not support video.', 306 | { 307 | inputFilePath: z.string().describe('Path to the audio file to play'), 308 | isUrl: z.boolean().optional().default(false).describe('Whether the audio file is a URL'), 309 | }, 310 | async (params) => { 311 | try { 312 | // No need to update configuration from request parameters in stdio mode 313 | const result = await playAudio(params); 314 | 315 | return { 316 | content: [ 317 | { 318 | type: 'text', 319 | text: result, 320 | }, 321 | ], 322 | }; 323 | } catch (error) { 324 | return { 325 | content: [ 326 | { 327 | type: 'text', 328 | text: `Failed to play audio: ${error instanceof Error ? error.message : String(error)}`, 329 | }, 330 | ], 331 | }; 332 | } 333 | }, 334 | ); 335 | } 336 | 337 | 338 | /** 339 | * Register voice clone tool 340 | */ 341 | private registerVoiceCloneTool(): void { 342 | this.server.tool( 343 | 'voice_clone', 344 | '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.', 345 | { 346 | voiceId: z.string().describe('Voice ID to use'), 347 | audioFile: z.string().describe('Path to the audio file'), 348 | text: z.string().optional().describe('Text for the demo audio'), 349 | outputDirectory: COMMON_PARAMETERS_SCHEMA.outputDirectory, 350 | isUrl: z.boolean().optional().default(false).describe('Whether the audio file is a URL'), 351 | }, 352 | async (params) => { 353 | try { 354 | // No need to update configuration from request parameters in stdio mode 355 | const result = await this.voiceCloneApi.cloneVoice(params); 356 | 357 | return { 358 | content: [ 359 | { 360 | type: 'text', 361 | text: `Voice cloning successful: ${result}`, 362 | }, 363 | ], 364 | }; 365 | } catch (error) { 366 | // 检查是否是实名认证错误 367 | const errorMessage = error instanceof Error ? error.message : String(error); 368 | if (errorMessage.includes('voice clone user forbidden') || 369 | errorMessage.includes('should complete real-name verification')) { 370 | 371 | // 国内平台认证URL 372 | const verificationUrl = 'https://platform.minimaxi.com/user-center/basic-information'; 373 | 374 | return { 375 | content: [ 376 | { 377 | type: 'text', 378 | 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.`, 379 | }, 380 | ], 381 | }; 382 | } 383 | 384 | // 其他错误的常规处理 385 | return { 386 | content: [ 387 | { 388 | type: 'text', 389 | text: `Voice cloning failed: ${error instanceof Error ? error.message : String(error)}`, 390 | }, 391 | ], 392 | }; 393 | } 394 | }, 395 | ); 396 | } 397 | 398 | /** 399 | * Register text-to-image tool 400 | */ 401 | private registerTextToImageTool(): void { 402 | this.server.tool( 403 | 'text_to_image', 404 | '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.', 405 | { 406 | model: z.string().optional().default(DEFAULT_T2I_MODEL).describe('Model to use'), 407 | prompt: z.string().describe('Text prompt for image generation'), 408 | aspectRatio: z 409 | .string() 410 | .optional() 411 | .default('1:1') 412 | .describe('Image aspect ratio, values: ["1:1", "16:9","4:3", "3:2", "2:3", "3:4", "9:16", "21:9"]'), 413 | n: z.number().min(1).max(9).optional().default(1).describe('Number of images to generate'), 414 | promptOptimizer: z.boolean().optional().default(true).describe('Whether to optimize the prompt'), 415 | outputDirectory: COMMON_PARAMETERS_SCHEMA.outputDirectory, 416 | outputFile: z 417 | .string() 418 | .optional() 419 | .describe('Path to save the generated image file, automatically generated if not provided'), 420 | }, 421 | async (params) => { 422 | try { 423 | // No need to update configuration from request parameters in stdio mode 424 | 425 | // If no output filename is provided, generate one automatically 426 | if (!params.outputFile) { 427 | const promptPrefix = params.prompt.substring(0, 20).replace(/[^\w]/g, '_'); 428 | params.outputFile = `image_${promptPrefix}_${Date.now()}`; 429 | } 430 | 431 | const outputFiles = await this.imageApi.generateImage(params); 432 | 433 | // Handle different output formats 434 | if (this.config.resourceMode === RESOURCE_MODE_URL) { 435 | return { 436 | content: [ 437 | { 438 | type: 'text', 439 | text: `Success. Image URL(s): ${outputFiles.join(', ')}`, 440 | }, 441 | ], 442 | }; 443 | } else { 444 | return { 445 | content: [ 446 | { 447 | type: 'text', 448 | text: `Image(s) saved: ${outputFiles.join(', ')}`, 449 | }, 450 | ], 451 | }; 452 | } 453 | } catch (error) { 454 | return { 455 | content: [ 456 | { 457 | type: 'text', 458 | text: `Failed to generate image: ${error instanceof Error ? error.message : String(error)}`, 459 | }, 460 | ], 461 | }; 462 | } 463 | }, 464 | ); 465 | } 466 | 467 | /** 468 | * Register generate video tool 469 | */ 470 | private registerGenerateVideoTool(): void { 471 | this.server.tool( 472 | 'generate_video', 473 | '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.', 474 | { 475 | model: z 476 | .string() 477 | .optional() 478 | .default(DEFAULT_VIDEO_MODEL) 479 | .describe('Model to use, values: ["T2V-01", "T2V-01-Director", "I2V-01", "I2V-01-Director", "I2V-01-live", "MiniMax-Hailuo-02"]'), 480 | prompt: z.string().describe('Text prompt for video generation'), 481 | firstFrameImage: z.string().optional().describe('First frame image'), 482 | duration: z.number().optional().describe('The duration of the video. The model must be "MiniMax-Hailuo-02". Values can be 6 and 10.'), 483 | resolution: z.string().optional().describe('The resolution of the video. The model must be "MiniMax-Hailuo-02". Values range ["768P", "1080P"]'), 484 | outputDirectory: COMMON_PARAMETERS_SCHEMA.outputDirectory, 485 | outputFile: z 486 | .string() 487 | .optional() 488 | .describe('Path to save the generated video file, automatically generated if not provided'), 489 | asyncMode: z 490 | .boolean() 491 | .optional() 492 | .default(false) 493 | .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.'), 494 | }, 495 | async (params) => { 496 | try { 497 | // No need to update configuration from request parameters in stdio mode 498 | 499 | // If no output filename is provided, generate one automatically 500 | if (!params.outputFile) { 501 | const promptPrefix = params.prompt.substring(0, 20).replace(/[^\w]/g, '_'); 502 | params.outputFile = `video_${promptPrefix}_${Date.now()}`; 503 | } 504 | 505 | const result = await this.videoApi.generateVideo(params); 506 | 507 | if (params.asyncMode) { 508 | return { 509 | content: [ 510 | { 511 | type: 'text', 512 | 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.`, 513 | }, 514 | ], 515 | }; 516 | } else { 517 | // Handle different output formats 518 | if (this.config.resourceMode === RESOURCE_MODE_URL) { 519 | return { 520 | content: [ 521 | { 522 | type: 'text', 523 | text: `Success. Video URL: ${result.video_url}`, 524 | }, 525 | ], 526 | }; 527 | } else { 528 | return { 529 | content: [ 530 | { 531 | type: 'text', 532 | text: `Video saved: ${result.video_path}`, 533 | }, 534 | ], 535 | }; 536 | } 537 | } 538 | } catch (error) { 539 | return { 540 | content: [ 541 | { 542 | type: 'text', 543 | text: `Failed to generate video: ${error instanceof Error ? error.message : String(error)}`, 544 | }, 545 | ], 546 | }; 547 | } 548 | }, 549 | ); 550 | } 551 | 552 | /** 553 | * Register image-to-video tool 554 | */ 555 | private registerImageToVideoTool(): void { 556 | this.server.tool( 557 | 'image_to_video', 558 | '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.', 559 | { 560 | model: z 561 | .string() 562 | .optional() 563 | .default('I2V-01') 564 | .describe('Model to use, values: ["I2V-01", "I2V-01-Director", "I2V-01-live"]'), 565 | prompt: z.string().describe('Text prompt for video generation'), 566 | firstFrameImage: z.string().describe('Path to the first frame image'), 567 | outputDirectory: COMMON_PARAMETERS_SCHEMA.outputDirectory, 568 | outputFile: z 569 | .string() 570 | .optional() 571 | .describe('Path to save the generated video file, automatically generated if not provided'), 572 | asyncMode: z 573 | .boolean() 574 | .optional() 575 | .default(false) 576 | .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.'), 577 | }, 578 | async (params) => { 579 | try { 580 | // If no output filename is provided, generate one automatically 581 | if (!params.outputFile) { 582 | const promptPrefix = params.prompt.substring(0, 20).replace(/[^\w]/g, '_'); 583 | params.outputFile = `i2v_${promptPrefix}_${Date.now()}`; 584 | } 585 | 586 | const result = await this.videoApi.generateVideo(params); 587 | 588 | if (params.asyncMode) { 589 | return { 590 | content: [ 591 | { 592 | type: 'text', 593 | 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.`, 594 | }, 595 | ], 596 | }; 597 | } 598 | 599 | // Handle different output formats 600 | if (this.config.resourceMode === RESOURCE_MODE_URL) { 601 | return { 602 | content: [ 603 | { 604 | type: 'text', 605 | text: `Success. Video URL: ${result.video_url}`, 606 | }, 607 | ], 608 | }; 609 | } else { 610 | return { 611 | content: [ 612 | { 613 | type: 'text', 614 | text: `Video saved: ${result.video_path}`, 615 | }, 616 | ], 617 | }; 618 | } 619 | } catch (error) { 620 | return { 621 | content: [ 622 | { 623 | type: 'text', 624 | text: `Failed to generate video: ${error instanceof Error ? error.message : String(error)}`, 625 | }, 626 | ], 627 | }; 628 | } 629 | } 630 | ); 631 | } 632 | 633 | /** 634 | * Register query video generation tool 635 | */ 636 | private registerQueryVideoGenerationTool(): void { 637 | this.server.tool( 638 | 'query_video_generation', 639 | 'Query the status of a video generation task.', 640 | { 641 | taskId: z 642 | .string() 643 | .describe('The Task ID to query. Should be the task_id returned by `generate_video` tool if `async_mode` is True.'), 644 | outputDirectory: COMMON_PARAMETERS_SCHEMA.outputDirectory, 645 | }, 646 | async (params) => { 647 | try { 648 | // No need to update configuration from request parameters in stdio mode 649 | const result = await this.videoApi.queryVideoGeneration(params); 650 | 651 | if (result.status === 'Success') { 652 | if (this.config.resourceMode === RESOURCE_MODE_URL) { 653 | return { 654 | content: [ 655 | { 656 | type: 'text', 657 | text: `Success. Video URL: ${result.video_url}`, 658 | }, 659 | ], 660 | }; 661 | } else { 662 | return { 663 | content: [ 664 | { 665 | type: 'text', 666 | text: `Success. Video saved as: ${result.video_path}`, 667 | }, 668 | ], 669 | }; 670 | } 671 | } else { 672 | return { 673 | content: [ 674 | { 675 | type: 'text', 676 | text: `Video generation task is still processing: Task ID: ${params.taskId}.`, 677 | }, 678 | ], 679 | }; 680 | } 681 | } catch (error) { 682 | return { 683 | content: [ 684 | { 685 | type: 'text', 686 | text: `Failed to query video generation: ${error instanceof Error ? error.message : String(error)}`, 687 | }, 688 | ], 689 | }; 690 | } 691 | }, 692 | ); 693 | } 694 | 695 | /** 696 | * Register music generation tool 697 | */ 698 | private registerMusicGenerationTool(): void { 699 | this.server.tool( 700 | 'music_generation', 701 | '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.', 702 | { 703 | prompt: z 704 | .string() 705 | .describe('Music creation inspiration describing style, mood, scene, etc.\nExample: "Pop music, sad, suitable for rainy nights". Character range: [10, 300]'), 706 | lyrics: z 707 | .string() 708 | .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)'), 709 | sampleRate: z 710 | .number() 711 | .optional() 712 | .default(DEFAULT_SAMPLE_RATE) 713 | .describe('Sample rate of generated music. Values: [16000, 24000, 32000, 44100]'), 714 | bitrate: z 715 | .number() 716 | .optional() 717 | .default(DEFAULT_BITRATE) 718 | .describe('Bitrate of generated music. Values: [32000, 64000, 128000, 256000]'), 719 | format: z 720 | .string() 721 | .optional() 722 | .default(DEFAULT_FORMAT) 723 | .describe('Format of generated music. Values: ["mp3", "wav", "pcm"]'), 724 | outputDirectory: COMMON_PARAMETERS_SCHEMA.outputDirectory, 725 | }, 726 | async (params) => { 727 | try { 728 | // Automatically set resource mode (if not specified) 729 | const outputFormat = this.config.resourceMode; 730 | const musicRequest = { 731 | ...params, 732 | outputFormat, 733 | }; 734 | 735 | // No need to update configuration from request parameters in stdio mode 736 | const outputFile = await this.musicApi.generateMusic(musicRequest); 737 | 738 | // Handle different output formats 739 | if (this.config.resourceMode === RESOURCE_MODE_URL) { 740 | return { 741 | content: [ 742 | { 743 | type: 'text', 744 | text: `Success. Music URL: ${outputFile}`, 745 | }, 746 | ], 747 | }; 748 | } else { 749 | return { 750 | content: [ 751 | { 752 | type: 'text', 753 | text: `Success. Music saved as: ${outputFile}`, 754 | }, 755 | ], 756 | }; 757 | } 758 | } catch (error) { 759 | return { 760 | content: [ 761 | { 762 | type: 'text', 763 | text: `Failed to generate music: ${error instanceof Error ? error.message : String(error)}`, 764 | }, 765 | ], 766 | }; 767 | } 768 | }, 769 | ); 770 | } 771 | 772 | /** 773 | * Register voice design tool 774 | */ 775 | private registerVoiceDesignTool(): void { 776 | this.server.tool( 777 | 'voice_design', 778 | '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.', 779 | { 780 | prompt: z 781 | .string() 782 | .describe('The prompt to generate the voice from'), 783 | previewText: z 784 | .string() 785 | .describe('The text to preview the voice'), 786 | voiceId: z 787 | .string() 788 | .optional() 789 | .describe('The id of the voice to use. For example, "male-qn-qingse"/"audiobook_female_1"/"cute_boy"/"Charming_Lady"...'), 790 | outputDirectory: COMMON_PARAMETERS_SCHEMA.outputDirectory, 791 | }, 792 | async (params) => { 793 | try { 794 | // No need to update configuration from request parameters in stdio mode 795 | const { voiceId, outputFile } = await this.voiceDesignApi.voiceDesign(params); 796 | 797 | // Handle different output formats 798 | if (this.config.resourceMode === RESOURCE_MODE_URL) { 799 | return { 800 | content: [ 801 | { 802 | type: 'text', 803 | text: `Success. Voice ID: ${voiceId}. Voice URL: ${outputFile}`, 804 | }, 805 | ], 806 | }; 807 | } else { 808 | return { 809 | content: [ 810 | { 811 | type: 'text', 812 | text: `Success. Voice ID: ${voiceId}. Voice saved as: ${outputFile}`, 813 | }, 814 | ], 815 | }; 816 | } 817 | } catch (error) { 818 | return { 819 | content: [ 820 | { 821 | type: 'text', 822 | text: `Failed to design voice: ${error instanceof Error ? error.message : String(error)}`, 823 | }, 824 | ], 825 | }; 826 | } 827 | }, 828 | ); 829 | } 830 | 831 | /** 832 | * Update configuration and recreate API instances 833 | * @param newConfig New configuration object 834 | */ 835 | public updateConfig(newConfig: Partial): void { 836 | // Merge server configuration 837 | if (newConfig.server && this.config.server) { 838 | // New configuration has higher priority and should override existing configuration 839 | this.config.server = { 840 | ...this.config.server, // Lower priority configuration loaded first 841 | ...newConfig.server // Higher priority configuration loaded last 842 | }; 843 | delete newConfig.server; 844 | } else if (newConfig.server) { 845 | this.config.server = newConfig.server; 846 | delete newConfig.server; 847 | } 848 | 849 | // Merge other configurations, new configuration has higher priority 850 | this.config = { 851 | ...this.config, // Lower priority configuration loaded first 852 | ...newConfig // Higher priority configuration loaded last 853 | }; 854 | 855 | // Update API instances 856 | this.api = new MiniMaxAPI(this.config); 857 | this.ttsApi = new TTSAPI(this.api); 858 | this.imageApi = new ImageAPI(this.api); 859 | this.videoApi = new VideoAPI(this.api); 860 | this.voiceCloneApi = new VoiceCloneAPI(this.api); 861 | this.voiceApi = new VoiceAPI(this.api); 862 | this.voiceDesignApi = new VoiceDesignAPI(this.api); 863 | this.musicApi = new MusicAPI(this.api); 864 | } 865 | 866 | /** 867 | * Read configuration from file 868 | * @returns Partial configuration object or undefined 869 | */ 870 | private getConfigFromFile(): Partial | undefined { 871 | try { 872 | // Prioritize configuration file path parameter 873 | const configPath = getParamValue("config_path") || process.env[ENV_CONFIG_PATH] || './minimax-config.json'; 874 | 875 | // Check if file exists 876 | if (!fs.existsSync(configPath)) { 877 | return undefined; 878 | } 879 | 880 | // Read and parse configuration file 881 | const fileContent = fs.readFileSync(configPath, 'utf8'); 882 | return JSON.parse(fileContent) as Partial; 883 | } catch (error) { 884 | // console.warn(`Failed to read config file: ${error instanceof Error ? error.message : String(error)}`); 885 | return undefined; 886 | } 887 | } 888 | 889 | /** 890 | * Start the server 891 | * @returns Promise to start the server 892 | */ 893 | public async start(): Promise { 894 | try { 895 | // Validate necessary configuration 896 | if (!this.config.apiKey) { 897 | throw new Error(ERROR_API_KEY_REQUIRED); 898 | } 899 | 900 | if (!this.config.apiHost) { 901 | throw new Error(ERROR_API_HOST_REQUIRED); 902 | } 903 | 904 | // Start STDIO server 905 | return this.startStdioServer(); 906 | } catch (error) { 907 | // console.error(`Failed to start server: ${error instanceof Error ? error.message : String(error)}`); 908 | throw error; 909 | } 910 | } 911 | 912 | /** 913 | * Start standard input/output server 914 | */ 915 | public async startStdioServer(): Promise { 916 | // console.log('Starting stdio server'); 917 | const transport = new StdioServerTransport(); 918 | await this.server.connect(transport); 919 | // console.log('MiniMax MCP Server running on stdio'); 920 | } 921 | 922 | /** 923 | * Get server instance 924 | * @returns McpServer instance 925 | */ 926 | public getServer(): McpServer { 927 | return this.server; 928 | } 929 | 930 | /** 931 | * Get current configuration 932 | * @returns Current configuration 933 | */ 934 | public getConfig(): Config { 935 | return this.config; 936 | } 937 | } 938 | 939 | // Export types and API instances for use in other projects 940 | export { 941 | Config, 942 | TTSRequest, 943 | ImageGenerationRequest, 944 | VideoGenerationRequest, 945 | VoiceCloneRequest, 946 | ListVoicesRequest, 947 | PlayAudioRequest, 948 | ServerOptions, 949 | TransportMode, 950 | } from './types/index.js'; 951 | export { MiniMaxAPI } from './utils/api.js'; 952 | export { TTSAPI } from './api/tts.js'; 953 | export { ImageAPI } from './api/image.js'; 954 | export { VideoAPI } from './api/video.js'; 955 | export { VoiceCloneAPI } from './api/voice-clone.js'; 956 | export { VoiceAPI } from './api/voice.js'; 957 | export * from './exceptions/index.js'; 958 | export * from './const/index.js'; 959 | --------------------------------------------------------------------------------