#
tokens: 27480/50000 17/17 files
lines: on (toggle) GitHub
raw markdown copy reset
# Directory Structure

```
├── .github
│   └── workflows
│       └── npm-publish.yml
├── .gitignore
├── assets
│   └── banner.png
├── CHANGELOG.md
├── LICENSE
├── package-lock.json
├── package.json
├── pnpm-lock.yaml
├── README.md
├── README.zh-CN.md
├── src
│   ├── config
│   │   ├── config-manager.ts
│   │   ├── constants.ts
│   │   └── schemas.ts
│   ├── core
│   │   ├── base-client.ts
│   │   ├── rate-limiter.ts
│   │   └── task-manager.ts
│   ├── index.ts
│   ├── services
│   │   ├── image-service.ts
│   │   └── tts-service.ts
│   └── utils
│       ├── error-handler.ts
│       └── file-handler.ts
└── tsconfig.json
```

# Files

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
 1 | # Dependency directories
 2 | node_modules/
 3 | npm-debug.log
 4 | yarn-debug.log
 5 | yarn-error.log
 6 | 
 7 | # Environment variables
 8 | .env
 9 | .env.local
10 | .env.development.local
11 | .env.test.local
12 | .env.production.local
13 | 
14 | # Generated files
15 | generated-images/
16 | generated-audio/
17 | 
18 | # Build output
19 | dist/
20 | build/
21 | 
22 | # IDE and editor files
23 | .idea/
24 | .vscode/
25 | *.swp
26 | *.swo
27 | .DS_Store
28 | 
29 | # Logs
30 | logs
31 | *.log
32 | npm-debug.log*
33 | yarn-debug.log*
34 | yarn-error.log*
35 | 
36 | # Coverage directory used by tools like istanbul
37 | coverage/
38 | 
39 | # nyc test coverage
40 | .nyc_output/
41 | 
42 | # Optional npm cache directory
43 | .npm
44 | 
45 | # Optional eslint cache
46 | .eslintcache
47 | 
48 | .mcp.json
49 | mcp.json
50 | 
```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
  1 | # Minimax MCP Tools
  2 | 
  3 | ![Banner](assets/banner.png)
  4 | 
  5 | A Model Context Protocol (MCP) server for Minimax AI integration, providing async image generation and text-to-speech with advanced rate limiting and error handling.
  6 | 
  7 | English | [简体中文](README.zh-CN.md)
  8 | 
  9 | ### MCP Configuration
 10 | Add to your MCP settings:
 11 | ```json
 12 | {
 13 |   "mcpServers": {
 14 |     "minimax-mcp-tools": {
 15 |       "command": "npx",
 16 |       "args": ["minimax-mcp-tools"],
 17 |       "env": {
 18 |         "MINIMAX_API_KEY": "your_api_key_here"
 19 |       }
 20 |     }
 21 |   }
 22 | }
 23 | ```
 24 | 
 25 | ## Async Design - Perfect for Content Production at Scale
 26 | 
 27 | This MCP server uses an **asynchronous submit-and-barrier pattern** designed for **batch content creation**:
 28 | 
 29 | 🎬 **Narrated Slideshow Production** - Generate dozens of slide images and corresponding narration in parallel  
 30 | 📚 **AI-Driven Audiobook Creation** - Produce chapters with multiple voice characters simultaneously  
 31 | 🖼️ **Website Asset Generation** - Create consistent visual content and audio elements for web projects  
 32 | 🎯 **Multimedia Content Pipelines** - Perfect for LLM-driven content workflows requiring both visuals and audio
 33 | 
 34 | ### Architecture Benefits:
 35 | 1. **Submit Phase**: Tools return immediately with task IDs, tasks execute in background
 36 | 2. **Smart Rate Limiting**: Adaptive rate limiting (10 RPM images, 20 RPM speech) with burst capacity 
 37 | 3. **Barrier Synchronization**: `task_barrier` waits for all tasks and returns comprehensive results
 38 | 4. **Batch Optimization**: Submit multiple tasks to saturate rate limits, then barrier once for maximum throughput
 39 | 
 40 | 
 41 | 
 42 | ## Tools
 43 | 
 44 | ### `submit_image_generation`
 45 | **Submit Image Generation Task** - Generate images asynchronously. 
 46 | 
 47 | **Required:** `prompt`, `outputFile`  
 48 | **Optional:** `aspectRatio`, `customSize`, `seed`, `subjectReference`, `style`
 49 | 
 50 | ### `submit_speech_generation`
 51 | **Submit Speech Generation Task** - Convert text to speech asynchronously.
 52 | 
 53 | **Required:** `text`, `outputFile`  
 54 | **Optional:** `highQuality`, `voiceId`, `speed`, `volume`, `pitch`, `emotion`, `format`, `sampleRate`, `bitrate`, `languageBoost`, `intensity`, `timbre`, `sound_effects`
 55 | 
 56 | ### `task_barrier`
 57 | **Wait for Task Completion** - Wait for ALL submitted tasks to complete and retrieve results. Essential for batch processing.
 58 | 
 59 | ## Architecture
 60 | ```mermaid
 61 | sequenceDiagram
 62 |     participant User
 63 |     participant MCP as MCP Server
 64 |     participant TM as Task Manager
 65 |     participant API as Minimax API
 66 | 
 67 |     Note over User, API: Async Submit-and-Barrier Pattern
 68 | 
 69 |     User->>MCP: submit_image_generation(prompt1)
 70 |     MCP->>TM: submitImageTask()
 71 |     TM-->>MCP: taskId: img-001
 72 |     MCP-->>User: "Task img-001 submitted"
 73 |     
 74 |     par Background Execution (Rate Limited)
 75 |         TM->>API: POST /image/generate
 76 |         API-->>TM: image data + save file
 77 |     end
 78 | 
 79 |     User->>MCP: submit_speech_generation(text1)
 80 |     MCP->>TM: submitTTSTask()
 81 |     TM-->>MCP: taskId: tts-002
 82 |     MCP-->>User: "Task tts-002 submitted"
 83 |     
 84 |     par Background Execution (Rate Limited)
 85 |         TM->>API: POST /speech/generate
 86 |         API-->>TM: audio data + save file
 87 |     end
 88 | 
 89 |     User->>MCP: submit_image_generation(prompt2)
 90 |     MCP->>TM: submitImageTask()
 91 |     TM-->>MCP: taskId: img-003
 92 |     MCP-->>User: "Task img-003 submitted"
 93 | 
 94 |     par Background Execution (Rate Limited)
 95 |         TM->>API: POST /image/generate (queued)
 96 |         API-->>TM: image data + save file
 97 |     end
 98 | 
 99 |     User->>MCP: task_barrier()
100 |     MCP->>TM: barrier()
101 |     TM->>TM: wait for all tasks
102 |     TM-->>MCP: results summary
103 |     MCP-->>User: ✅ All tasks completed<br/>Files available at specified paths
104 | 
105 |     Note over User, API: Immediate Task Submission + Background Rate-Limited Execution
106 | ```
107 | 
108 | ## License
109 | MIT
```

--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------

```markdown
 1 | # Changelog
 2 | 
 3 | ## [2.2.0] - 2025-08-14
 4 | 
 5 | ### Added
 6 | - Speech 2.5 series models (`speech-2.5-hd-preview`, `speech-2.5-turbo-preview`)
 7 | - 13 additional language boost options
 8 | 
 9 | ### Changed
10 | - **BREAKING**: Removed Speech 2.0 series models
11 | - Default model: `speech-02-hd` → `speech-2.5-hd-preview`
12 | 
13 | ### Fixed
14 | - Task barrier bug returning 0 completed tasks
15 | 
```

--------------------------------------------------------------------------------
/.github/workflows/npm-publish.yml:
--------------------------------------------------------------------------------

```yaml
 1 | # This workflow will build TypeScript and publish a package to npm when a release is created
 2 | # For more information see: https://docs.github.com/en/actions/publishing-packages/publishing-nodejs-packages
 3 | 
 4 | name: Build and Publish
 5 | 
 6 | on:
 7 |   release:
 8 |     types: [created]
 9 | 
10 | jobs:
11 |   publish-npm:
12 |     runs-on: ubuntu-latest
13 |     steps:
14 |       - uses: actions/checkout@v4
15 |       
16 |       - uses: actions/setup-node@v4
17 |         with:
18 |           node-version: 20
19 |           registry-url: https://registry.npmjs.org/
20 |           
21 |       - name: Install pnpm
22 |         uses: pnpm/action-setup@v3
23 |         with:
24 |           version: 8
25 |           
26 |       - name: Install dependencies
27 |         run: pnpm install --no-frozen-lockfile
28 |         
29 |       - name: Build TypeScript
30 |         run: pnpm run build
31 |         
32 |       - name: Publish to npm
33 |         run: pnpm publish --no-git-checks
34 |         env:
35 |           NODE_AUTH_TOKEN: ${{secrets.npm_token}}
36 | 
```

--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "compilerOptions": {
 3 |     "target": "ES2022",
 4 |     "module": "ESNext",
 5 |     "moduleResolution": "Node",
 6 |     "lib": ["ES2022", "DOM"],
 7 |     "outDir": "./dist",
 8 |     "rootDir": "./src",
 9 |     "strict": true,
10 |     "esModuleInterop": true,
11 |     "skipLibCheck": true,
12 |     "forceConsistentCasingInFileNames": true,
13 |     "declaration": true,
14 |     "declarationMap": true,
15 |     "sourceMap": true,
16 |     "removeComments": true,
17 |     "allowSyntheticDefaultImports": true,
18 |     "resolveJsonModule": true,
19 |     "experimentalDecorators": true,
20 |     "emitDecoratorMetadata": true,
21 |     "noImplicitAny": true,
22 |     "noImplicitReturns": true,
23 |     "noUnusedLocals": true,
24 |     "noUnusedParameters": true,
25 |     "exactOptionalPropertyTypes": true,
26 |     "noImplicitOverride": true,
27 |     "noPropertyAccessFromIndexSignature": true,
28 |     "noUncheckedIndexedAccess": true
29 |   },
30 |   "include": [
31 |     "src/**/*"
32 |   ],
33 |   "exclude": [
34 |     "node_modules",
35 |     "dist",
36 |     "**/*.test.ts",
37 |     "**/*.spec.ts"
38 |   ],
39 |   "ts-node": {
40 |     "esm": true,
41 |     "experimentalSpecifierResolution": "node"
42 |   }
43 | }
```

--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "name": "minimax-mcp-tools",
 3 |   "version": "2.2.1",
 4 |   "description": "Async MCP server with Minimax API integration for image generation and text-to-speech",
 5 |   "type": "module",
 6 |   "main": "dist/index.js",
 7 |   "bin": {
 8 |     "minimax-mcp-tools": "dist/index.js"
 9 |   },
10 |   "scripts": {
11 |     "build": "tsc",
12 |     "start": "node dist/index.js",
13 |     "dev": "ts-node src/index.ts",
14 |     "dev:watch": "nodemon --exec ts-node src/index.ts",
15 |     "test": "echo \"Error: no test specified\" && exit 1",
16 |     "prepublishOnly": "npm run build"
17 |   },
18 |   "keywords": [
19 |     "mcp",
20 |     "minimax",
21 |     "ai",
22 |     "image-generation",
23 |     "text-to-speech",
24 |     "tts"
25 |   ],
26 |   "author": "PsychArch (https://github.com/PsychArch)",
27 |   "license": "MIT",
28 |   "repository": {
29 |     "type": "git",
30 |     "url": "git+https://github.com/PsychArch/minimax-mcp-tools.git"
31 |   },
32 |   "bugs": {
33 |     "url": "https://github.com/PsychArch/minimax-mcp-tools/issues"
34 |   },
35 |   "homepage": "https://github.com/PsychArch/minimax-mcp-tools#readme",
36 |   "dependencies": {
37 |     "@modelcontextprotocol/sdk": "^1.17.0",
38 |     "node-fetch": "^3.3.2",
39 |     "zod": "^3.25.76"
40 |   },
41 |   "devDependencies": {
42 |     "@types/node": "^20.19.9",
43 |     "nodemon": "^3.0.0",
44 |     "ts-node": "^10.9.0",
45 |     "typescript": "^5.3.0"
46 |   },
47 |   "engines": {
48 |     "node": ">=16.0.0"
49 |   },
50 |   "files": [
51 |     "dist/",
52 |     "src/",
53 |     "README.md",
54 |     "README.zh-CN.md",
55 |     "LICENSE",
56 |     "assets/"
57 |   ],
58 |   "publishConfig": {
59 |     "access": "public"
60 |   }
61 | }
62 | 
```

--------------------------------------------------------------------------------
/src/config/config-manager.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { MinimaxConfigError } from '../utils/error-handler.js';
  2 | 
  3 | interface Config {
  4 |   apiKey: string;
  5 |   apiHost: string;
  6 |   logLevel: 'error' | 'debug';
  7 |   tempDir: string;
  8 |   maxConcurrency: number;
  9 |   retryAttempts: number;
 10 |   retryDelay: number;
 11 | }
 12 | 
 13 | interface RetryConfig {
 14 |   attempts: number;
 15 |   delay: number;
 16 | }
 17 | 
 18 | export class ConfigManager {
 19 |   private static instance: ConfigManager | null = null;
 20 |   private config!: Config;
 21 | 
 22 |   constructor() {
 23 |     if (ConfigManager.instance) {
 24 |       return ConfigManager.instance;
 25 |     }
 26 |     
 27 |     this.config = this.loadConfig();
 28 |     ConfigManager.instance = this;
 29 |   }
 30 | 
 31 |   static getInstance(): ConfigManager {
 32 |     if (!ConfigManager.instance) {
 33 |       ConfigManager.instance = new ConfigManager();
 34 |     }
 35 |     return ConfigManager.instance;
 36 |   }
 37 | 
 38 |   private loadConfig(): Config {
 39 |     return {
 40 |       apiKey: this.getRequiredEnv('MINIMAX_API_KEY'),
 41 |       apiHost: 'https://api.minimaxi.com',
 42 |       logLevel: 'error',
 43 |       tempDir: '/tmp',
 44 |       maxConcurrency: 5,
 45 |       retryAttempts: 3,
 46 |       retryDelay: 1000
 47 |     };
 48 |   }
 49 | 
 50 |   private getRequiredEnv(key: string): string {
 51 |     const value = process.env[key];
 52 |     if (!value) {
 53 |       throw new MinimaxConfigError(`Required environment variable ${key} is not set`);
 54 |     }
 55 |     return value;
 56 |   }
 57 | 
 58 |   get<K extends keyof Config>(key: K): Config[K] {
 59 |     return this.config[key];
 60 |   }
 61 | 
 62 |   getApiKey(): string {
 63 |     return this.config.apiKey;
 64 |   }
 65 | 
 66 |   getApiHost(): string | undefined {
 67 |     return this.config.apiHost;
 68 |   }
 69 | 
 70 |   getTempDir(): string {
 71 |     return this.config.tempDir;
 72 |   }
 73 | 
 74 |   getMaxConcurrency(): number {
 75 |     return this.config.maxConcurrency;
 76 |   }
 77 | 
 78 |   getRetryConfig(): RetryConfig {
 79 |     return {
 80 |       attempts: this.config.retryAttempts,
 81 |       delay: this.config.retryDelay
 82 |     };
 83 |   }
 84 | 
 85 |   isDebugMode(): boolean {
 86 |     return this.config.logLevel === 'debug';
 87 |   }
 88 | 
 89 |   // Validate configuration
 90 |   validate(): boolean {
 91 |     const required: Array<keyof Config> = ['apiKey'];
 92 |     const missing = required.filter(key => !this.config[key]);
 93 |     
 94 |     if (missing.length > 0) {
 95 |       throw new MinimaxConfigError(`Missing required configuration: ${missing.join(', ')}`);
 96 |     }
 97 | 
 98 |     return true;
 99 |   }
100 | }
```

--------------------------------------------------------------------------------
/src/core/rate-limiter.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { MinimaxRateLimitError } from '../utils/error-handler.js';
  2 | 
  3 | interface RateLimiterConfig {
  4 |   rpm: number;
  5 |   burst?: number;
  6 |   window?: number;
  7 | }
  8 | 
  9 | interface AdaptiveRateLimiterConfig extends RateLimiterConfig {
 10 |   backoffFactor?: number;
 11 |   recoveryFactor?: number;
 12 |   maxBackoff?: number;
 13 | }
 14 | 
 15 | interface QueueRequest {
 16 |   resolve: () => void;
 17 |   reject: (error: Error) => void;
 18 |   timestamp: number;
 19 | }
 20 | 
 21 | interface RateLimiterStatus {
 22 |   tokens: number;
 23 |   queueLength: number;
 24 |   rpm: number;
 25 |   burst: number;
 26 | }
 27 | 
 28 | export interface AdaptiveStatus extends RateLimiterStatus {
 29 |   consecutiveErrors: number;
 30 |   adaptedRpm: number;
 31 |   originalRpm: number;
 32 | }
 33 | 
 34 | export class RateLimiter {
 35 |   protected rpm: number;
 36 |   protected burst: number;
 37 |   protected window: number;
 38 |   protected interval: number;
 39 |   protected tokens: number;
 40 |   protected lastRefill: number;
 41 |   protected queue: QueueRequest[];
 42 | 
 43 |   constructor({ rpm, burst = 1, window = 60000 }: RateLimiterConfig) {
 44 |     this.rpm = rpm;
 45 |     this.burst = burst;
 46 |     this.window = window;
 47 |     this.interval = window / rpm;
 48 |     
 49 |     // Token bucket algorithm
 50 |     this.tokens = burst;
 51 |     this.lastRefill = Date.now();
 52 |     this.queue = [];
 53 |   }
 54 | 
 55 |   async acquire(): Promise<void> {
 56 |     return new Promise<void>((resolve, reject) => {
 57 |       this.queue.push({ resolve, reject, timestamp: Date.now() });
 58 |       this.processQueue();
 59 |     });
 60 |   }
 61 | 
 62 |   private processQueue(): void {
 63 |     if (this.queue.length === 0) return;
 64 | 
 65 |     this.refillTokens();
 66 | 
 67 |     while (this.queue.length > 0 && this.tokens > 0) {
 68 |       const request = this.queue.shift();
 69 |       if (!request) break;
 70 |       
 71 |       this.tokens--;
 72 |       
 73 |       // Schedule the next refill
 74 |       const delay = Math.max(0, this.interval - (Date.now() - this.lastRefill));
 75 |       setTimeout(() => this.processQueue(), delay);
 76 |       
 77 |       request.resolve();
 78 |     }
 79 |   }
 80 | 
 81 |   private refillTokens(): void {
 82 |     const now = Date.now();
 83 |     const timePassed = now - this.lastRefill;
 84 |     const tokensToAdd = Math.floor(timePassed / this.interval);
 85 |     
 86 |     if (tokensToAdd > 0) {
 87 |       this.tokens = Math.min(this.burst, this.tokens + tokensToAdd);
 88 |       this.lastRefill = now;
 89 |     }
 90 |   }
 91 | 
 92 |   getStatus(): RateLimiterStatus {
 93 |     this.refillTokens();
 94 |     return {
 95 |       tokens: this.tokens,
 96 |       queueLength: this.queue.length,
 97 |       rpm: this.rpm,
 98 |       burst: this.burst
 99 |     };
100 |   }
101 | 
102 |   reset(): void {
103 |     this.tokens = this.burst;
104 |     this.lastRefill = Date.now();
105 |     this.queue = [];
106 |   }
107 | }
108 | 
109 | export class AdaptiveRateLimiter extends RateLimiter {
110 |   private consecutiveErrors: number;
111 |   private originalRpm: number;
112 |   private backoffFactor: number;
113 |   private recoveryFactor: number;
114 |   private maxBackoff: number;
115 | 
116 |   constructor(config: AdaptiveRateLimiterConfig) {
117 |     super(config);
118 |     this.consecutiveErrors = 0;
119 |     this.originalRpm = this.rpm;
120 |     this.backoffFactor = config.backoffFactor || 0.5;
121 |     this.recoveryFactor = config.recoveryFactor || 1.1;
122 |     this.maxBackoff = config.maxBackoff || 5;
123 |   }
124 | 
125 |   onSuccess(): void {
126 |     // Gradually recover rate limit after success
127 |     if (this.consecutiveErrors > 0) {
128 |       this.consecutiveErrors = Math.max(0, this.consecutiveErrors - 1);
129 |       
130 |       if (this.consecutiveErrors === 0) {
131 |         this.rpm = Math.min(this.originalRpm, this.rpm * this.recoveryFactor);
132 |         this.interval = this.window / this.rpm;
133 |       }
134 |     }
135 |   }
136 | 
137 |   onError(error: Error): void {
138 |     if (error instanceof MinimaxRateLimitError) {
139 |       this.consecutiveErrors++;
140 |       
141 |       // Reduce rate limit on consecutive errors
142 |       const backoffMultiplier = Math.pow(this.backoffFactor, Math.min(this.consecutiveErrors, this.maxBackoff));
143 |       this.rpm = Math.max(1, this.originalRpm * backoffMultiplier);
144 |       this.interval = this.window / this.rpm;
145 |       
146 |       // Clear some tokens to enforce the new limit
147 |       this.tokens = Math.min(this.tokens, Math.floor(this.burst * backoffMultiplier));
148 |     }
149 |   }
150 | 
151 |   getAdaptiveStatus(): AdaptiveStatus {
152 |     return {
153 |       ...this.getStatus(),
154 |       consecutiveErrors: this.consecutiveErrors,
155 |       adaptedRpm: this.rpm,
156 |       originalRpm: this.originalRpm
157 |     };
158 |   }
159 | }
```

--------------------------------------------------------------------------------
/src/core/base-client.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import fetch from 'node-fetch';
  2 | import type { RequestInit, Response } from 'node-fetch';
  3 | import { ConfigManager } from '../config/config-manager.js';
  4 | import { API_CONFIG } from '../config/constants.js';
  5 | import { ErrorHandler, MinimaxError } from '../utils/error-handler.js';
  6 | 
  7 | interface BaseClientOptions {
  8 |   baseURL?: string;
  9 |   timeout?: number;
 10 | }
 11 | 
 12 | interface RequestOptions extends Omit<RequestInit, 'body'> {
 13 |   body?: any;
 14 |   headers?: Record<string, string>;
 15 | }
 16 | 
 17 | interface HealthCheckResult {
 18 |   status: 'healthy' | 'unhealthy';
 19 |   timestamp: string;
 20 |   error?: string;
 21 | }
 22 | 
 23 | interface APIResponse {
 24 |   base_resp?: {
 25 |     status_code: number;
 26 |     status_msg?: string;
 27 |   };
 28 |   [key: string]: any;
 29 | }
 30 | 
 31 | export class MinimaxBaseClient {
 32 |   protected config: ConfigManager;
 33 |   protected baseURL: string;
 34 |   protected timeout: number;
 35 |   protected retryConfig: { attempts: number; delay: number };
 36 | 
 37 |   constructor(options: BaseClientOptions = {}) {
 38 |     this.config = ConfigManager.getInstance();
 39 |     this.baseURL = options.baseURL || API_CONFIG.BASE_URL;
 40 |     this.timeout = options.timeout || API_CONFIG.TIMEOUT;
 41 |     this.retryConfig = this.config.getRetryConfig();
 42 |   }
 43 | 
 44 |   async makeRequest(endpoint: string, options: RequestOptions = {}): Promise<APIResponse> {
 45 |     const url = `${this.baseURL}${endpoint}`;
 46 |     const headers: Record<string, string> = {
 47 |       'Authorization': `Bearer ${this.config.getApiKey()}`,
 48 |       'Content-Type': 'application/json',
 49 |       ...API_CONFIG.HEADERS,
 50 |       ...options.headers
 51 |     };
 52 | 
 53 |     const requestOptions: RequestInit & { timeout?: number } = {
 54 |       method: options.method || 'POST',
 55 |       headers,
 56 |       timeout: this.timeout,
 57 |       ...options
 58 |     };
 59 | 
 60 |     if (options.body && requestOptions.method !== 'GET') {
 61 |       requestOptions.body = JSON.stringify(options.body);
 62 |     }
 63 | 
 64 |     return this.executeWithRetry(url, requestOptions);
 65 |   }
 66 | 
 67 |   private async executeWithRetry(url: string, requestOptions: RequestInit & { timeout?: number }, attempt: number = 1): Promise<APIResponse> {
 68 |     try {
 69 |       const controller = new AbortController();
 70 |       const timeoutId = setTimeout(() => controller.abort(), this.timeout);
 71 | 
 72 |       const response: Response = await fetch(url, {
 73 |         ...requestOptions,
 74 |         signal: controller.signal
 75 |       });
 76 | 
 77 |       clearTimeout(timeoutId);
 78 | 
 79 |       if (!response.ok) {
 80 |         const errorText = await response.text();
 81 |         throw new Error(`HTTP ${response.status}: ${errorText}`);
 82 |       }
 83 | 
 84 |       const data = await response.json() as APIResponse;
 85 |       return this.processResponse(data);
 86 | 
 87 |     } catch (error: any) {
 88 |       const processedError = ErrorHandler.handleAPIError(error);
 89 | 
 90 |       // Retry logic for certain errors
 91 |       if (this.shouldRetry(processedError, attempt)) {
 92 |         await this.delay(this.retryConfig.delay * attempt);
 93 |         return this.executeWithRetry(url, requestOptions, attempt + 1);
 94 |       }
 95 | 
 96 |       throw processedError;
 97 |     }
 98 |   }
 99 | 
100 |   private processResponse(data: APIResponse): APIResponse {
101 |     // Check for API-level errors in response
102 |     if (data.base_resp && data.base_resp.status_code !== 0) {
103 |       throw ErrorHandler.handleAPIError(new Error('API Error'), data);
104 |     }
105 | 
106 |     return data;
107 |   }
108 | 
109 |   private shouldRetry(error: MinimaxError, attempt: number): boolean {
110 |     if (attempt >= this.retryConfig.attempts) {
111 |       return false;
112 |     }
113 | 
114 |     // Retry on network errors, timeouts, and 5xx errors
115 |     return (
116 |       error.code === 'NETWORK_ERROR' ||
117 |       error.code === 'TIMEOUT_ERROR' ||
118 |       ('statusCode' in error && typeof error.statusCode === 'number' && error.statusCode >= 500 && error.statusCode < 600)
119 |     );
120 |   }
121 | 
122 |   private async delay(ms: number): Promise<void> {
123 |     return new Promise(resolve => setTimeout(resolve, ms));
124 |   }
125 | 
126 |   async get(endpoint: string, options: RequestOptions = {}): Promise<APIResponse> {
127 |     return this.makeRequest(endpoint, { ...options, method: 'GET' });
128 |   }
129 | 
130 |   async post(endpoint: string, body?: any, options: RequestOptions = {}): Promise<APIResponse> {
131 |     return this.makeRequest(endpoint, { ...options, method: 'POST', body });
132 |   }
133 | 
134 |   // Health check method
135 |   async healthCheck(): Promise<HealthCheckResult> {
136 |     try {
137 |       // Make a simple request to verify connectivity
138 |       await this.get('/health');
139 |       return { status: 'healthy', timestamp: new Date().toISOString() };
140 |     } catch (error: any) {
141 |       return { 
142 |         status: 'unhealthy', 
143 |         error: ErrorHandler.formatErrorForUser(error),
144 |         timestamp: new Date().toISOString() 
145 |       };
146 |     }
147 |   }
148 | }
```

--------------------------------------------------------------------------------
/src/utils/file-handler.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import fs from 'fs/promises';
  2 | import path from 'path';
  3 | import fetch from 'node-fetch';
  4 | import type { RequestInit } from 'node-fetch';
  5 | import { MinimaxError } from './error-handler.js';
  6 | 
  7 | interface DownloadOptions {
  8 |   timeout?: number;
  9 |   fetchOptions?: RequestInit;
 10 | }
 11 | 
 12 | interface FileStats {
 13 |   size: number;
 14 |   isFile(): boolean;
 15 |   isDirectory(): boolean;
 16 |   mtime: Date;
 17 |   ctime: Date;
 18 | }
 19 | 
 20 | export class FileHandler {
 21 |   static async ensureDirectoryExists(filePath: string): Promise<void> {
 22 |     const dir = path.dirname(filePath);
 23 |     try {
 24 |       await fs.mkdir(dir, { recursive: true });
 25 |     } catch (error: any) {
 26 |       throw new MinimaxError(`Failed to create directory: ${error.message}`);
 27 |     }
 28 |   }
 29 | 
 30 |   static async writeFile(filePath: string, data: string | Buffer, options: any = {}): Promise<void> {
 31 |     try {
 32 |       await this.ensureDirectoryExists(filePath);
 33 |       await fs.writeFile(filePath, data, options);
 34 |     } catch (error: any) {
 35 |       throw new MinimaxError(`Failed to write file ${filePath}: ${error.message}`);
 36 |     }
 37 |   }
 38 | 
 39 |   static async readFile(filePath: string, options: any = {}): Promise<Buffer | string> {
 40 |     try {
 41 |       return await fs.readFile(filePath, options);
 42 |     } catch (error: any) {
 43 |       throw new MinimaxError(`Failed to read file ${filePath}: ${error.message}`);
 44 |     }
 45 |   }
 46 | 
 47 |   static async downloadFile(url: string, outputPath: string, options: DownloadOptions = {}): Promise<string> {
 48 |     try {
 49 |       await this.ensureDirectoryExists(outputPath);
 50 |       
 51 |       const response = await fetch(url, {
 52 |         ...options.fetchOptions
 53 |       });
 54 | 
 55 |       if (!response.ok) {
 56 |         throw new Error(`HTTP ${response.status}: ${response.statusText}`);
 57 |       }
 58 | 
 59 |       const buffer = await response.buffer();
 60 |       await fs.writeFile(outputPath, buffer);
 61 |       
 62 |       return outputPath;
 63 |     } catch (error: any) {
 64 |       throw new MinimaxError(`Failed to download file from ${url}: ${error.message}`);
 65 |     }
 66 |   }
 67 | 
 68 |   static async convertToBase64(input: string): Promise<string> {
 69 |     try {
 70 |       let buffer: Buffer;
 71 |       
 72 |       if (input.startsWith('http://') || input.startsWith('https://')) {
 73 |         // Download URL and convert to base64
 74 |         const response = await fetch(input);
 75 |         if (!response.ok) {
 76 |           throw new Error(`HTTP ${response.status}: ${response.statusText}`);
 77 |         }
 78 |         buffer = await response.buffer();
 79 |       } else {
 80 |         // Read local file
 81 |         const fileData = await this.readFile(input);
 82 |         buffer = Buffer.isBuffer(fileData) ? fileData : Buffer.from(fileData as string);
 83 |       }
 84 |       
 85 |       return `data:image/jpeg;base64,${buffer.toString('base64')}`;
 86 |     } catch (error: any) {
 87 |       throw new MinimaxError(`Failed to convert to base64: ${error.message}`);
 88 |     }
 89 |   }
 90 | 
 91 |   static generateUniqueFilename(basePath: string, index: number, total: number): string {
 92 |     if (total === 1) {
 93 |       return basePath;
 94 |     }
 95 |     
 96 |     const dir = path.dirname(basePath);
 97 |     const ext = path.extname(basePath);
 98 |     const name = path.basename(basePath, ext);
 99 |     
100 |     return path.join(dir, `${name}_${String(index + 1).padStart(2, '0')}${ext}`);
101 |   }
102 | 
103 |   static validateFilePath(filePath: string): boolean {
104 |     if (!filePath || typeof filePath !== 'string') {
105 |       throw new MinimaxError('File path must be a non-empty string');
106 |     }
107 |     
108 |     if (!path.isAbsolute(filePath)) {
109 |       throw new MinimaxError('File path must be absolute');
110 |     }
111 |     
112 |     return true;
113 |   }
114 | 
115 |   static getFileExtension(format: string): string {
116 |     const extensions: Record<string, string> = {
117 |       mp3: '.mp3',
118 |       wav: '.wav',
119 |       flac: '.flac',
120 |       pcm: '.pcm',
121 |       jpg: '.jpg',
122 |       jpeg: '.jpeg',
123 |       png: '.png',
124 |       webp: '.webp'
125 |     };
126 |     
127 |     return extensions[format.toLowerCase()] || `.${format}`;
128 |   }
129 | 
130 |   static async fileExists(filePath: string): Promise<boolean> {
131 |     try {
132 |       await fs.access(filePath);
133 |       return true;
134 |     } catch {
135 |       return false;
136 |     }
137 |   }
138 | 
139 |   static async getFileStats(filePath: string): Promise<FileStats> {
140 |     try {
141 |       const stats = await fs.stat(filePath);
142 |       return {
143 |         size: stats.size,
144 |         isFile: () => stats.isFile(),
145 |         isDirectory: () => stats.isDirectory(),
146 |         mtime: stats.mtime,
147 |         ctime: stats.ctime
148 |       };
149 |     } catch (error: any) {
150 |       throw new MinimaxError(`Failed to get file stats: ${error.message}`);
151 |     }
152 |   }
153 | 
154 |   static async saveBase64Image(base64Data: string, outputPath: string): Promise<void> {
155 |     try {
156 |       await this.ensureDirectoryExists(outputPath);
157 |       
158 |       // Remove data URL prefix if present
159 |       const cleanBase64 = base64Data.replace(/^data:image\/\w+;base64,/, '');
160 |       const buffer = Buffer.from(cleanBase64, 'base64');
161 |       
162 |       await fs.writeFile(outputPath, buffer);
163 |     } catch (error: any) {
164 |       throw new MinimaxError(`Failed to save base64 image: ${error.message}`);
165 |     }
166 |   }
167 | }
```

--------------------------------------------------------------------------------
/src/services/image-service.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { MinimaxBaseClient } from '../core/base-client.js';
  2 | import { API_CONFIG, DEFAULTS, MODELS, CONSTRAINTS, type ImageModel } from '../config/constants.js';
  3 | import { FileHandler } from '../utils/file-handler.js';
  4 | import { ErrorHandler } from '../utils/error-handler.js';
  5 | import { type ImageGenerationParams } from '../config/schemas.js';
  6 | 
  7 | interface ImageGenerationPayload {
  8 |   model: string;
  9 |   prompt: string;
 10 |   n: number;
 11 |   prompt_optimizer: boolean;
 12 |   response_format: string;
 13 |   width?: number;
 14 |   height?: number;
 15 |   aspect_ratio?: string;
 16 |   seed?: number;
 17 |   subject_reference?: Array<{
 18 |     type: string;
 19 |     image_file: string;
 20 |   }>;
 21 |   style?: {
 22 |     style_type: string;
 23 |     style_weight: number;
 24 |   };
 25 | }
 26 | 
 27 | interface ImageGenerationResponse {
 28 |   data?: {
 29 |     image_urls?: string[];
 30 |     image_base64?: string[];
 31 |   };
 32 | }
 33 | 
 34 | interface ImageGenerationResult {
 35 |   files: string[];
 36 |   count: number;
 37 |   model: string;
 38 |   prompt: string;
 39 |   warnings?: string[];
 40 | }
 41 | 
 42 | export class ImageGenerationService extends MinimaxBaseClient {
 43 |   constructor(options: { baseURL?: string; timeout?: number } = {}) {
 44 |     super(options);
 45 |   }
 46 | 
 47 |   async generateImage(params: ImageGenerationParams): Promise<ImageGenerationResult> {
 48 |     try {
 49 |       // Build API payload (MCP handles validation)
 50 |       const payload = this.buildPayload(params);
 51 |       
 52 |       // Make API request
 53 |       const response = await this.post(API_CONFIG.ENDPOINTS.IMAGE_GENERATION, payload) as ImageGenerationResponse;
 54 |       
 55 |       // Process response
 56 |       return await this.processImageResponse(response, params);
 57 |       
 58 |     } catch (error: any) {
 59 |       const processedError = ErrorHandler.handleAPIError(error);
 60 |       ErrorHandler.logError(processedError, { service: 'image', params });
 61 |       
 62 |       // Throw the error so task manager can properly mark it as failed
 63 |       throw processedError;
 64 |     }
 65 |   }
 66 | 
 67 |   private buildPayload(params: ImageGenerationParams): ImageGenerationPayload {
 68 |     const imageDefaults = DEFAULTS.IMAGE as any;
 69 |     
 70 |     // Choose model based on whether style is provided
 71 |     const model = params.style ? 'image-01-live' : 'image-01';
 72 |     
 73 |     const payload: ImageGenerationPayload = {
 74 |       model: model,
 75 |       prompt: params.prompt,
 76 |       n: 1,
 77 |       prompt_optimizer: true, // Always optimize prompts
 78 |       response_format: 'url' // Always use URL since we save to file
 79 |     };
 80 | 
 81 |     // Handle sizing parameters (conflict-free approach)
 82 |     if (params.customSize) {
 83 |       payload.width = params.customSize.width;
 84 |       payload.height = params.customSize.height;
 85 |     } else {
 86 |       payload.aspect_ratio = params.aspectRatio || imageDefaults.aspectRatio;
 87 |     }
 88 | 
 89 |     // Add optional parameters
 90 |     if (params.seed !== undefined) {
 91 |       payload.seed = params.seed;
 92 |     }
 93 | 
 94 |     // Model-specific parameter handling
 95 |     if (model === 'image-01') {
 96 |       // Add subject reference for image-01 model
 97 |       // MCP Server Bridge: Convert user-friendly file path to API format
 98 |       if (params.subjectReference) {
 99 |         // TODO: Convert file path/URL to base64 or ensure URL is accessible
100 |         // For now, pass through assuming it's already in correct format
101 |         payload.subject_reference = [{
102 |           type: 'character',
103 |           image_file: params.subjectReference
104 |         }];
105 |       }
106 |     } else if (model === 'image-01-live') {
107 |       // Add style settings for image-01-live model
108 |       if (params.style) {
109 |         payload.style = {
110 |           style_type: params.style.style_type,
111 |           style_weight: params.style.style_weight || 0.8
112 |         };
113 |       }
114 |     }
115 | 
116 | 
117 |     return payload;
118 |   }
119 | 
120 |   private async processImageResponse(response: ImageGenerationResponse, params: ImageGenerationParams): Promise<ImageGenerationResult> {
121 |     // Handle both URL and base64 responses
122 |     const imageUrls = response.data?.image_urls || [];
123 |     const imageBase64 = response.data?.image_base64 || [];
124 |     
125 |     if (!imageUrls.length && !imageBase64.length) {
126 |       throw new Error('No images generated in API response');
127 |     }
128 | 
129 |     // Download and save images
130 |     const savedFiles: string[] = [];
131 |     const errors: string[] = [];
132 |     const imageSources = imageUrls.length ? imageUrls : imageBase64;
133 | 
134 |     for (let i = 0; i < imageSources.length; i++) {
135 |       try {
136 |         const filename = FileHandler.generateUniqueFilename(params.outputFile, i, imageSources.length);
137 |         
138 |         if (imageBase64.length && !imageUrls.length) {
139 |           // Save base64 image
140 |           await FileHandler.saveBase64Image(imageBase64[i]!, filename);
141 |         } else {
142 |           // Download from URL
143 |           await FileHandler.downloadFile(imageSources[i]!, filename);
144 |         }
145 |         
146 |         savedFiles.push(filename);
147 |       } catch (error: any) {
148 |         errors.push(`Image ${i + 1}: ${error.message}`);
149 |       }
150 |     }
151 | 
152 |     if (savedFiles.length === 0) {
153 |       throw new Error(`Failed to save any images: ${errors.join('; ')}`);
154 |     }
155 | 
156 |     // Use the actual model that was used
157 |     const modelUsed = params.style ? 'image-01-live' : 'image-01';
158 |     
159 |     const result: ImageGenerationResult = {
160 |       files: savedFiles,
161 |       count: savedFiles.length,
162 |       model: modelUsed,
163 |       prompt: params.prompt
164 |     };
165 | 
166 |     if (errors.length > 0) {
167 |       result.warnings = errors;
168 |     }
169 | 
170 |     return result;
171 |   }
172 | 
173 |   // Utility methods
174 |   async validateSubjectReference(reference: string): Promise<string | null> {
175 |     if (!reference) return null;
176 |     
177 |     try {
178 |       return await FileHandler.convertToBase64(reference);
179 |     } catch (error: any) {
180 |       throw new Error(`Invalid subject reference: ${error.message}`);
181 |     }
182 |   }
183 | 
184 |   getSupportedModels(): string[] {
185 |     return Object.keys(MODELS.IMAGE);
186 |   }
187 | 
188 |   getSupportedAspectRatios(): readonly string[] {
189 |     return CONSTRAINTS.IMAGE.ASPECT_RATIOS;
190 |   }
191 | 
192 |   getModelInfo(modelName: string): { name: string; description: string } | null {
193 |     return MODELS.IMAGE[modelName as ImageModel] || null;
194 |   }
195 | 
196 | }
```

--------------------------------------------------------------------------------
/src/utils/error-handler.ts:
--------------------------------------------------------------------------------

```typescript
  1 | // Custom error classes for better error handling
  2 | export class MinimaxError extends Error {
  3 |   public readonly code: string;
  4 |   public readonly details: any;
  5 |   public readonly timestamp: string;
  6 | 
  7 |   constructor(message: string, code: string = 'MINIMAX_ERROR', details: any = null) {
  8 |     super(message);
  9 |     this.name = this.constructor.name;
 10 |     this.code = code;
 11 |     this.details = details;
 12 |     this.timestamp = new Date().toISOString();
 13 |   }
 14 | 
 15 |   toJSON(): {
 16 |     name: string;
 17 |     message: string;
 18 |     code: string;
 19 |     details: any;
 20 |     timestamp: string;
 21 |   } {
 22 |     return {
 23 |       name: this.name,
 24 |       message: this.message,
 25 |       code: this.code,
 26 |       details: this.details,
 27 |       timestamp: this.timestamp
 28 |     };
 29 |   }
 30 | }
 31 | 
 32 | export class MinimaxConfigError extends MinimaxError {
 33 |   constructor(message: string, details: any = null) {
 34 |     super(message, 'CONFIG_ERROR', details);
 35 |   }
 36 | }
 37 | 
 38 | export class MinimaxAPIError extends MinimaxError {
 39 |   public readonly statusCode: number | null;
 40 |   public readonly response: any;
 41 | 
 42 |   constructor(message: string, statusCode: number | null = null, response: any = null) {
 43 |     super(message, 'API_ERROR', { statusCode, response });
 44 |     this.statusCode = statusCode;
 45 |     this.response = response;
 46 |   }
 47 | }
 48 | 
 49 | export class MinimaxValidationError extends MinimaxError {
 50 |   public readonly field: string | null;
 51 |   public readonly value: any;
 52 | 
 53 |   constructor(message: string, field: string | null = null, value: any = null) {
 54 |     super(message, 'VALIDATION_ERROR', { field, value });
 55 |     this.field = field;
 56 |     this.value = value;
 57 |   }
 58 | }
 59 | 
 60 | export class MinimaxNetworkError extends MinimaxError {
 61 |   public readonly originalError: Error | null;
 62 | 
 63 |   constructor(message: string, originalError: Error | null = null) {
 64 |     super(message, 'NETWORK_ERROR', { originalError: originalError?.message });
 65 |     this.originalError = originalError;
 66 |   }
 67 | }
 68 | 
 69 | export class MinimaxTimeoutError extends MinimaxError {
 70 |   public readonly timeout: number | null;
 71 | 
 72 |   constructor(message: string, timeout: number | null = null) {
 73 |     super(message, 'TIMEOUT_ERROR', { timeout });
 74 |     this.timeout = timeout;
 75 |   }
 76 | }
 77 | 
 78 | export class MinimaxRateLimitError extends MinimaxError {
 79 |   public readonly retryAfter: number | null;
 80 | 
 81 |   constructor(message: string, retryAfter: number | null = null) {
 82 |     super(message, 'RATE_LIMIT_ERROR', { retryAfter });
 83 |     this.retryAfter = retryAfter;
 84 |   }
 85 | }
 86 | 
 87 | // API Response interface for better typing
 88 | interface APIResponse {
 89 |   base_resp?: {
 90 |     status_code: number;
 91 |     status_msg?: string;
 92 |     retry_after?: number;
 93 |   };
 94 | }
 95 | 
 96 | // Error with common Node.js error properties
 97 | interface NodeError extends Error {
 98 |   code?: string;
 99 |   timeout?: number;
100 | }
101 | 
102 | // Error handler utility functions
103 | export class ErrorHandler {
104 |   static handleAPIError(error: NodeError, response?: APIResponse): MinimaxError {
105 |     // Handle different types of API errors
106 |     if (response?.base_resp && response.base_resp.status_code !== 0) {
107 |       const statusCode = response.base_resp.status_code;
108 |       const message = response.base_resp.status_msg || 'API request failed';
109 |       
110 |       switch (statusCode) {
111 |         case 1004:
112 |           return new MinimaxAPIError(`Authentication failed: ${message}`, statusCode, response);
113 |         case 1013:
114 |           return new MinimaxRateLimitError(`Rate limit exceeded: ${message}`, response.base_resp?.retry_after);
115 |         default:
116 |           return new MinimaxAPIError(message, statusCode, response);
117 |       }
118 |     }
119 | 
120 |     // Handle HTTP errors
121 |     if (error.message && error.message.includes('HTTP')) {
122 |       const match = error.message.match(/HTTP (\d+):/);
123 |       const statusCode = match ? parseInt(match[1]!, 10) : null;
124 |       
125 |       switch (statusCode) {
126 |         case 401:
127 |           return new MinimaxAPIError('Unauthorized: Invalid API key', statusCode!);
128 |         case 403:
129 |           return new MinimaxAPIError('Forbidden: Access denied', statusCode!);
130 |         case 404:
131 |           return new MinimaxAPIError('Not found: Invalid endpoint', statusCode!);
132 |         case 429:
133 |           return new MinimaxRateLimitError('Rate limit exceeded', null);
134 |         case 500:
135 |           return new MinimaxAPIError('Internal server error', statusCode!);
136 |         default:
137 |           return new MinimaxAPIError(error.message, statusCode!);
138 |       }
139 |     }
140 | 
141 |     // Handle network errors
142 |     if (error.code === 'ECONNREFUSED' || error.code === 'ENOTFOUND') {
143 |       return new MinimaxNetworkError('Network connection failed', error);
144 |     }
145 | 
146 |     // Handle timeout errors
147 |     if (error.name === 'AbortError' || (error.message && error.message.includes('timeout'))) {
148 |       return new MinimaxTimeoutError('Request timeout', error.timeout);
149 |     }
150 | 
151 |     // Default to generic error
152 |     return new MinimaxError(error.message || 'Unknown error occurred');
153 |   }
154 | 
155 |   static formatErrorForUser(error: Error): string {
156 |     if (error instanceof MinimaxConfigError) {
157 |       return `Configuration Error: ${error.message}`;
158 |     }
159 |     
160 |     if (error instanceof MinimaxValidationError) {
161 |       return `Validation Error: ${error.message}`;
162 |     }
163 |     
164 |     if (error instanceof MinimaxAPIError) {
165 |       return `API Error: ${error.message}`;
166 |     }
167 |     
168 |     if (error instanceof MinimaxNetworkError) {
169 |       return `Network Error: ${error.message}`;
170 |     }
171 |     
172 |     if (error instanceof MinimaxTimeoutError) {
173 |       return `Timeout Error: ${error.message}`;
174 |     }
175 |     
176 |     if (error instanceof MinimaxRateLimitError) {
177 |       return `Rate Limit Error: ${error.message}`;
178 |     }
179 |     
180 |     return `Error: ${error.message}`;
181 |   }
182 | 
183 |   static logError(error: Error, context: Record<string, any> = {}): void {
184 |     const logEntry = {
185 |       timestamp: new Date().toISOString(),
186 |       error: error instanceof MinimaxError ? error.toJSON() : {
187 |         name: error.name,
188 |         message: error.message,
189 |         stack: error.stack
190 |       },
191 |       context
192 |     };
193 |     
194 |     if (typeof console !== 'undefined') {
195 |       console.error('[MINIMAX-ERROR]', JSON.stringify(logEntry, null, 2));
196 |     }
197 |   }
198 | }
```

--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------

```typescript
  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 | 
  6 | // Import refactored components
  7 | import { ConfigManager } from './config/config-manager.js';
  8 | import { 
  9 |   imageGenerationSchema,
 10 |   textToSpeechSchema,
 11 |   taskBarrierSchema,
 12 |   validateImageParams,
 13 |   validateTTSParams,
 14 |   validateTaskBarrierParams
 15 | } from './config/schemas.js';
 16 | import { ImageGenerationService } from './services/image-service.js';
 17 | import { TextToSpeechService } from './services/tts-service.js';
 18 | import { RateLimitedTaskManager } from './core/task-manager.js';
 19 | import { ErrorHandler } from './utils/error-handler.js';
 20 | 
 21 | // MCP Tool Response interface
 22 | interface ToolResponse {
 23 |   [x: string]: unknown;
 24 |   content: Array<{
 25 |     type: "text";
 26 |     text: string;
 27 |   }>;
 28 | }
 29 | 
 30 | // Initialize configuration and services
 31 | let config: ConfigManager;
 32 | let imageService: ImageGenerationService;
 33 | let ttsService: TextToSpeechService;
 34 | let taskManager: RateLimitedTaskManager;
 35 | 
 36 | try {
 37 |   config = ConfigManager.getInstance();
 38 |   config.validate();
 39 |   
 40 |   imageService = new ImageGenerationService();
 41 |   ttsService = new TextToSpeechService();
 42 |   taskManager = new RateLimitedTaskManager();
 43 |   
 44 | } catch (error: any) {
 45 |   console.error("❌ Failed to initialize:", ErrorHandler.formatErrorForUser(error));
 46 |   process.exit(1);
 47 | }
 48 | 
 49 | // Create MCP server
 50 | const server = new McpServer({
 51 |   name: "minimax-mcp-tools",
 52 |   version: "2.2.0",
 53 |   description: "Async Minimax AI integration for image generation and text-to-speech"
 54 | });
 55 | 
 56 | // Image generation tool
 57 | server.registerTool(
 58 |   "submit_image_generation",
 59 |   {
 60 |     title: "Submit Image Generation Task",
 61 |     description: "Generate images asynchronously. RECOMMENDED: Submit multiple tasks in batch to saturate rate limits, then call task_barrier once to wait for all completions. Returns task ID only - actual files available after task_barrier.",
 62 |     inputSchema: imageGenerationSchema.shape
 63 |   },
 64 |   async (params: unknown): Promise<ToolResponse> => {
 65 |     try {
 66 |       const validatedParams = validateImageParams(params);
 67 |       const { taskId } = await taskManager.submitImageTask(async () => {
 68 |         return await imageService.generateImage(validatedParams);
 69 |       });
 70 | 
 71 |       return {
 72 |         content: [{
 73 |           type: "text",
 74 |           text: `Task ${taskId} submitted`
 75 |         }]
 76 |       };
 77 |     } catch (error: any) {
 78 |       ErrorHandler.logError(error, { tool: 'submit_image_generation', params });
 79 |       return {
 80 |         content: [{
 81 |           type: "text",
 82 |           text: `❌ Failed to submit image generation task: ${ErrorHandler.formatErrorForUser(error)}`
 83 |         }]
 84 |       };
 85 |     }
 86 |   }
 87 | );
 88 | 
 89 | // Text-to-speech tool
 90 | server.registerTool(
 91 |   "submit_speech_generation",
 92 |   {
 93 |     title: "Submit Speech Generation Task", 
 94 |     description: "Convert text to speech asynchronously. RECOMMENDED: Submit multiple tasks in batch to saturate rate limits, then call task_barrier once to wait for all completions. Returns task ID only - actual files available after task_barrier.",
 95 |     inputSchema: textToSpeechSchema.shape
 96 |   },
 97 |   async (params: unknown): Promise<ToolResponse> => {
 98 |     try {
 99 |       const validatedParams = validateTTSParams(params);
100 |       const { taskId } = await taskManager.submitTTSTask(async () => {
101 |         return await ttsService.generateSpeech(validatedParams);
102 |       });
103 | 
104 |       return {
105 |         content: [{
106 |           type: "text",
107 |           text: `Task ${taskId} submitted`
108 |         }]
109 |       };
110 |     } catch (error: any) {
111 |       ErrorHandler.logError(error, { tool: 'submit_speech_generation', params });
112 |       return {
113 |         content: [{
114 |           type: "text",
115 |           text: `❌ Failed to submit TTS task: ${ErrorHandler.formatErrorForUser(error)}`
116 |         }]
117 |       };
118 |     }
119 |   }
120 | );
121 | 
122 | // Task barrier tool
123 | server.registerTool(
124 |   "task_barrier",
125 |   {
126 |     title: "Wait for Task Completion",
127 |     description: "Wait for ALL submitted tasks to complete and retrieve results. Essential for batch processing - submit multiple tasks first, then call task_barrier once to collect all results efficiently. Clears completed tasks.",
128 |     inputSchema: taskBarrierSchema.shape
129 |   },
130 |   async (params: unknown): Promise<ToolResponse> => {
131 |     try {
132 |       validateTaskBarrierParams(params);
133 |       const { completed, results } = await taskManager.barrier();
134 |       
135 |       if (completed === 0) {
136 |         return {
137 |           content: [{
138 |             type: "text",
139 |             text: "ℹ️ No tasks were submitted before this barrier."
140 |           }]
141 |         };
142 |       }
143 | 
144 |       // Format results
145 |       const resultSummaries = results.map(({ taskId, success, result, error }) => {
146 |         if (!success) {
147 |           return `❌ Task ${taskId}: FAILED - ${error?.message || 'Unknown error'}`;
148 |         }
149 | 
150 |         // Format success results based on task type
151 |         if (result?.files) {
152 |           // Image generation result
153 |           const warnings = result.warnings ? ` (${result.warnings.length} warnings)` : '';
154 |           return `✅ Task ${taskId}: Generated ${result.count} image(s)${warnings}`;
155 |         } else if (result?.audioFile) {
156 |           // TTS generation result
157 |           const subtitles = result.subtitleFile ? ` + subtitles` : '';
158 |           const warnings = result.warnings ? ` (${result.warnings.length} warnings)` : '';
159 |           return `✅ Task ${taskId}: Generated speech${subtitles}${warnings}`;
160 |         } else {
161 |           // Generic success
162 |           return `✅ Task ${taskId}: Completed successfully`;
163 |         }
164 |       });
165 | 
166 |       const summary = resultSummaries.join('\n');
167 | 
168 |       // Clear completed tasks to prevent memory leaks
169 |       taskManager.clearCompletedTasks();
170 | 
171 |       return {
172 |         content: [{
173 |           type: "text",
174 |           text: summary
175 |         }]
176 |       };
177 |     } catch (error: any) {
178 |       ErrorHandler.logError(error, { tool: 'task_barrier' });
179 |       return {
180 |         content: [{
181 |           type: "text",
182 |           text: `❌ Task barrier failed: ${ErrorHandler.formatErrorForUser(error)}`
183 |         }]
184 |       };
185 |     }
186 |   }
187 | );
188 | 
189 | 
190 | // Graceful shutdown
191 | process.on('SIGINT', () => {
192 |   console.error("🛑 Shutting down gracefully...");
193 |   taskManager.clearCompletedTasks();
194 |   process.exit(0);
195 | });
196 | 
197 | process.on('SIGTERM', () => {
198 |   console.error("🛑 Received SIGTERM, shutting down...");
199 |   taskManager.clearCompletedTasks();
200 |   process.exit(0);
201 | });
202 | 
203 | // Start server
204 | const transport = new StdioServerTransport();
205 | await server.connect(transport);
206 | 
```

--------------------------------------------------------------------------------
/src/core/task-manager.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { AdaptiveRateLimiter } from './rate-limiter.js';
  2 | import { RATE_LIMITS } from '../config/constants.js';
  3 | import { MinimaxError, ErrorHandler } from '../utils/error-handler.js';
  4 | 
  5 | // Type definitions
  6 | interface TaskResult {
  7 |   success: boolean;
  8 |   result?: any;
  9 |   error?: MinimaxError;
 10 |   completedAt: number;
 11 | }
 12 | 
 13 | interface TaskSubmissionResult {
 14 |   taskId: string;
 15 |   promise: Promise<any>;
 16 | }
 17 | 
 18 | interface BarrierResult {
 19 |   completed: number;
 20 |   results: (TaskResult & { taskId: string })[];
 21 | }
 22 | 
 23 | interface TaskStatus {
 24 |   status: 'running' | 'completed' | 'not_found';
 25 |   taskId: string;
 26 |   success?: boolean;
 27 |   result?: any;
 28 |   error?: MinimaxError;
 29 |   completedAt?: number;
 30 | }
 31 | 
 32 | interface AllTasksStatus {
 33 |   running: Array<{ taskId: string; status: 'running' }>;
 34 |   completed: Array<{ taskId: string; status: 'completed' } & TaskResult>;
 35 |   total: number;
 36 | }
 37 | 
 38 | interface TaskStats {
 39 |   activeTasks: number;
 40 |   completedTasks: number;
 41 |   totalProcessed: number;
 42 | }
 43 | 
 44 | interface TaskMetrics {
 45 |   requests: number;
 46 |   successes: number;
 47 |   errors: number;
 48 | }
 49 | 
 50 | interface RateLimitedTaskManagerOptions {
 51 |   backoffFactor?: number;
 52 |   recoveryFactor?: number;
 53 | }
 54 | 
 55 | export class TaskManager {
 56 |   protected tasks: Map<string, Promise<any>>;
 57 |   protected completedTasks: Map<string, TaskResult>;
 58 |   protected taskCounter: number;
 59 | 
 60 |   constructor() {
 61 |     this.tasks = new Map();
 62 |     this.completedTasks = new Map();
 63 |     this.taskCounter = 0;
 64 |   }
 65 | 
 66 |   protected generateTaskId(): string {
 67 |     return `task_${++this.taskCounter}`;
 68 |   }
 69 | 
 70 |   async submit(fn: () => Promise<any>, taskId: string | null = null): Promise<TaskSubmissionResult> {
 71 |     taskId = taskId || this.generateTaskId();
 72 |     
 73 |     const taskPromise = Promise.resolve()
 74 |       .then(fn)
 75 |       .then(result => {
 76 |         this.completedTasks.set(taskId!, { success: true, result, completedAt: Date.now() });
 77 |         return result;
 78 |       })
 79 |       .catch(error => {
 80 |         const processedError = ErrorHandler.handleAPIError(error);
 81 |         this.completedTasks.set(taskId!, { success: false, error: processedError, completedAt: Date.now() });
 82 |         throw processedError;
 83 |       })
 84 |       .finally(() => {
 85 |         this.tasks.delete(taskId!);
 86 |       });
 87 | 
 88 |     this.tasks.set(taskId, taskPromise);
 89 |     return { taskId, promise: taskPromise };
 90 |   }
 91 | 
 92 |   async barrier(): Promise<BarrierResult> {
 93 |     const activeTasks = Array.from(this.tasks.values());
 94 |     
 95 |     // Wait for any active tasks to complete
 96 |     if (activeTasks.length > 0) {
 97 |       await Promise.allSettled(activeTasks);
 98 |     }
 99 | 
100 |     // Return all completed tasks (including those completed before this barrier call)
101 |     const results = Array.from(this.completedTasks.entries()).map(([taskId, taskResult]) => ({
102 |       taskId,
103 |       ...taskResult
104 |     }));
105 | 
106 |     return { completed: results.length, results };
107 |   }
108 | 
109 |   getTaskStatus(taskId: string): TaskStatus {
110 |     if (this.tasks.has(taskId)) {
111 |       return { status: 'running', taskId };
112 |     }
113 |     
114 |     if (this.completedTasks.has(taskId)) {
115 |       return { status: 'completed', taskId, ...this.completedTasks.get(taskId)! };
116 |     }
117 |     
118 |     return { status: 'not_found', taskId };
119 |   }
120 | 
121 |   getAllTasksStatus(): AllTasksStatus {
122 |     const running = Array.from(this.tasks.keys()).map(taskId => ({ taskId, status: 'running' as const }));
123 |     const completed = Array.from(this.completedTasks.entries()).map(([taskId, result]) => ({
124 |       taskId,
125 |       status: 'completed' as const,
126 |       ...result
127 |     }));
128 |     
129 |     return { running, completed, total: running.length + completed.length };
130 |   }
131 | 
132 |   clearCompletedTasks(): number {
133 |     const count = this.completedTasks.size;
134 |     this.completedTasks.clear();
135 |     return count;
136 |   }
137 | 
138 |   getStats(): TaskStats {
139 |     return {
140 |       activeTasks: this.tasks.size,
141 |       completedTasks: this.completedTasks.size,
142 |       totalProcessed: this.taskCounter
143 |     };
144 |   }
145 | }
146 | 
147 | export class RateLimitedTaskManager extends TaskManager {
148 |   private rateLimiters: {
149 |     image: AdaptiveRateLimiter;
150 |     tts: AdaptiveRateLimiter;
151 |   };
152 |   private metrics: {
153 |     image: TaskMetrics;
154 |     tts: TaskMetrics;
155 |   };
156 |   private taskCounters: {
157 |     image: number;
158 |     tts: number;
159 |   };
160 | 
161 |   constructor(options: RateLimitedTaskManagerOptions = {}) {
162 |     super();
163 |     
164 |     this.rateLimiters = {
165 |       image: new AdaptiveRateLimiter({
166 |         ...RATE_LIMITS.IMAGE,
167 |         backoffFactor: options.backoffFactor || 0.7,
168 |         recoveryFactor: options.recoveryFactor || 1.05
169 |       }),
170 |       tts: new AdaptiveRateLimiter({
171 |         ...RATE_LIMITS.TTS,
172 |         backoffFactor: options.backoffFactor || 0.7,
173 |         recoveryFactor: options.recoveryFactor || 1.05
174 |       })
175 |     };
176 | 
177 |     this.metrics = {
178 |       image: { requests: 0, successes: 0, errors: 0 },
179 |       tts: { requests: 0, successes: 0, errors: 0 }
180 |     };
181 | 
182 |     this.taskCounters = {
183 |       image: 0,
184 |       tts: 0
185 |     };
186 |   }
187 | 
188 |   async submitImageTask(fn: () => Promise<any>, taskId: string | null = null): Promise<TaskSubmissionResult> {
189 |     if (!taskId) {
190 |       taskId = `img-${++this.taskCounters.image}`;
191 |     }
192 |     return this.submitRateLimitedTask('image', fn, taskId);
193 |   }
194 | 
195 |   async submitTTSTask(fn: () => Promise<any>, taskId: string | null = null): Promise<TaskSubmissionResult> {
196 |     if (!taskId) {
197 |       taskId = `tts-${++this.taskCounters.tts}`;
198 |     }
199 |     return this.submitRateLimitedTask('tts', fn, taskId);
200 |   }
201 | 
202 |   private async submitRateLimitedTask(type: 'image' | 'tts', fn: () => Promise<any>, taskId: string | null = null): Promise<TaskSubmissionResult> {
203 |     const rateLimiter = this.rateLimiters[type];
204 |     if (!rateLimiter) {
205 |       throw new MinimaxError(`Unknown task type: ${type}`);
206 |     }
207 | 
208 |     const wrappedFn = async () => {
209 |       await rateLimiter.acquire();
210 |       this.metrics[type].requests++;
211 |       
212 |       try {
213 |         const result = await fn();
214 |         this.metrics[type].successes++;
215 |         rateLimiter.onSuccess();
216 |         return result;
217 |       } catch (error: any) {
218 |         this.metrics[type].errors++;
219 |         rateLimiter.onError(error);
220 |         throw error;
221 |       }
222 |     };
223 | 
224 |     return this.submit(wrappedFn, taskId);
225 |   }
226 | 
227 |   getRateLimiterStatus() {
228 |     return {
229 |       image: this.rateLimiters.image.getAdaptiveStatus(),
230 |       tts: this.rateLimiters.tts.getAdaptiveStatus()
231 |     };
232 |   }
233 | 
234 |   getMetrics() {
235 |     return {
236 |       ...this.metrics,
237 |       rateLimiters: this.getRateLimiterStatus()
238 |     };
239 |   }
240 | 
241 |   resetMetrics(): void {
242 |     this.metrics = {
243 |       image: { requests: 0, successes: 0, errors: 0 },
244 |       tts: { requests: 0, successes: 0, errors: 0 }
245 |     };
246 | 
247 |     this.taskCounters = {
248 |       image: 0,
249 |       tts: 0
250 |     };
251 | 
252 |     Object.values(this.rateLimiters).forEach(limiter => limiter.reset());
253 |   }
254 | }
```

--------------------------------------------------------------------------------
/src/services/tts-service.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { MinimaxBaseClient } from '../core/base-client.js';
  2 | import { API_CONFIG, DEFAULTS, MODELS, VOICES, type TTSModel, type VoiceId } from '../config/constants.js';
  3 | import { FileHandler } from '../utils/file-handler.js';
  4 | import { ErrorHandler } from '../utils/error-handler.js';
  5 | import { type TextToSpeechParams } from '../config/schemas.js';
  6 | 
  7 | interface TTSPayload {
  8 |   model: string;
  9 |   text: string;
 10 |   voice_setting: {
 11 |     voice_id: string;
 12 |     speed: number;
 13 |     vol: number;
 14 |     pitch: number;
 15 |     emotion: string;
 16 |   };
 17 |   audio_setting: {
 18 |     sample_rate: number;
 19 |     bitrate: number;
 20 |     format: string;
 21 |     channel: number;
 22 |   };
 23 |   language_boost?: string;
 24 |   voice_modify?: {
 25 |     pitch?: number;
 26 |     intensity?: number;
 27 |     timbre?: number;
 28 |     sound_effects?: string;
 29 |   };
 30 | }
 31 | 
 32 | interface TTSResponse {
 33 |   data?: {
 34 |     audio?: string;
 35 |     duration?: number;
 36 |     subtitle_url?: string;
 37 |   };
 38 | }
 39 | 
 40 | interface TTSResult {
 41 |   audioFile: string;
 42 |   voiceUsed: string;
 43 |   model: string;
 44 |   duration: number | null;
 45 |   format: string;
 46 |   sampleRate: number;
 47 |   bitrate: number;
 48 |   subtitleFile?: string;
 49 |   warnings?: string[];
 50 | }
 51 | 
 52 | 
 53 | export class TextToSpeechService extends MinimaxBaseClient {
 54 |   constructor(options: { baseURL?: string; timeout?: number } = {}) {
 55 |     super(options);
 56 |   }
 57 | 
 58 |   async generateSpeech(params: TextToSpeechParams): Promise<TTSResult> {
 59 |     try {
 60 |       // Build API payload (MCP handles validation)
 61 |       const payload = this.buildPayload(params);
 62 |       
 63 |       // Make API request
 64 |       const response = await this.post(API_CONFIG.ENDPOINTS.TEXT_TO_SPEECH, payload) as TTSResponse;
 65 |       
 66 |       // Process response
 67 |       return await this.processTTSResponse(response, params);
 68 |       
 69 |     } catch (error: any) {
 70 |       const processedError = ErrorHandler.handleAPIError(error);
 71 |       ErrorHandler.logError(processedError, { service: 'tts', params });
 72 |       
 73 |       // Throw the error so task manager can properly mark it as failed
 74 |       throw processedError;
 75 |     }
 76 |   }
 77 | 
 78 |   private buildPayload(params: TextToSpeechParams): TTSPayload {
 79 |     const ttsDefaults = DEFAULTS.TTS as any;
 80 |     // Map highQuality parameter to appropriate Speech 2.6 model
 81 |     const model = (params as any).highQuality ? 'speech-2.6-hd' : 'speech-2.6-turbo';
 82 |     const payload: TTSPayload = {
 83 |       model: model,
 84 |       text: params.text,
 85 |       voice_setting: {
 86 |         voice_id: params.voiceId || ttsDefaults.voiceId,
 87 |         speed: params.speed || ttsDefaults.speed,
 88 |         vol: params.volume || ttsDefaults.volume,
 89 |         pitch: params.pitch || ttsDefaults.pitch,
 90 |         emotion: params.emotion || ttsDefaults.emotion
 91 |       },
 92 |       audio_setting: {
 93 |         sample_rate: parseInt(params.sampleRate || ttsDefaults.sampleRate),
 94 |         bitrate: parseInt(params.bitrate || ttsDefaults.bitrate),
 95 |         format: params.format || ttsDefaults.format,
 96 |         channel: ttsDefaults.channel
 97 |       }
 98 |     };
 99 | 
100 |     // Add optional parameters
101 |     if (params.languageBoost) {
102 |       payload.language_boost = params.languageBoost;
103 |     }
104 |     
105 |     // Add voice modify parameters if present
106 |     if (params.intensity !== undefined || params.timbre !== undefined || params.sound_effects !== undefined) {
107 |       payload.voice_modify = {};
108 |       
109 |       if (params.intensity !== undefined) {
110 |         payload.voice_modify.intensity = params.intensity;
111 |       }
112 |       
113 |       if (params.timbre !== undefined) {
114 |         payload.voice_modify.timbre = params.timbre;
115 |       }
116 |       
117 |       if (params.sound_effects !== undefined) {
118 |         payload.voice_modify.sound_effects = params.sound_effects;
119 |       }
120 |     }
121 | 
122 |     // Voice mixing feature removed for simplicity
123 | 
124 |     // Filter out undefined values
125 |     return this.cleanPayload(payload) as TTSPayload;
126 |   }
127 | 
128 |   private cleanPayload(obj: any): any {
129 |     if (typeof obj !== 'object' || obj === null) {
130 |       return obj;
131 |     }
132 | 
133 |     if (Array.isArray(obj)) {
134 |       return obj.map(item => this.cleanPayload(item)).filter(item => item !== undefined);
135 |     }
136 | 
137 |     const result: any = {};
138 |     for (const [key, value] of Object.entries(obj)) {
139 |       if (value === undefined) continue;
140 | 
141 |       if (typeof value === 'object' && value !== null) {
142 |         const cleanedValue = this.cleanPayload(value);
143 |         if (typeof cleanedValue === 'object' && !Array.isArray(cleanedValue) && Object.keys(cleanedValue).length === 0) {
144 |           continue;
145 |         }
146 |         result[key] = cleanedValue;
147 |       } else {
148 |         result[key] = value;
149 |       }
150 |     }
151 | 
152 |     return result;
153 |   }
154 | 
155 |   private async processTTSResponse(response: TTSResponse, params: TextToSpeechParams): Promise<TTSResult> {
156 |     const audioHex = response.data?.audio;
157 |     
158 |     if (!audioHex) {
159 |       throw new Error('No audio data received from API');
160 |     }
161 | 
162 |     // Convert hex to bytes and save
163 |     const audioBytes = Buffer.from(audioHex, 'hex');
164 |     await FileHandler.writeFile(params.outputFile, audioBytes);
165 | 
166 |     const ttsDefaults = DEFAULTS.TTS as any;
167 |     const result: TTSResult = {
168 |       audioFile: params.outputFile,
169 |       voiceUsed: params.voiceId || ttsDefaults.voiceId,
170 |       model: (params as any).highQuality ? 'speech-2.6-hd' : 'speech-2.6-turbo',
171 |       duration: response.data?.duration || null,
172 |       format: params.format || ttsDefaults.format,
173 |       sampleRate: parseInt(params.sampleRate || ttsDefaults.sampleRate),
174 |       bitrate: parseInt(params.bitrate || ttsDefaults.bitrate)
175 |     };
176 | 
177 |     // Subtitles feature removed for simplicity
178 | 
179 |     return result;
180 |   }
181 | 
182 |   // Utility methods
183 |   getSupportedModels(): string[] {
184 |     return Object.keys(MODELS.TTS);
185 |   }
186 | 
187 |   getSupportedVoices(): string[] {
188 |     return Object.keys(VOICES);
189 |   }
190 | 
191 |   getVoiceInfo(voiceId: string): { name: string; gender: 'male' | 'female' | 'other'; language: 'zh' | 'en' } | null {
192 |     return VOICES[voiceId as VoiceId] || null;
193 |   }
194 | 
195 |   getModelInfo(modelName: string): { name: string; description: string } | null {
196 |     return MODELS.TTS[modelName as TTSModel] || null;
197 |   }
198 | 
199 | 
200 |   validateVoiceParameters(params: TextToSpeechParams): string[] {
201 |     const ttsDefaults = DEFAULTS.TTS as any;
202 |     const voice = this.getVoiceInfo(params.voiceId || ttsDefaults.voiceId);
203 |     const model = (params as any).highQuality ? 'speech-2.6-hd' : 'speech-2.6-turbo';
204 | 
205 |     const issues: string[] = [];
206 | 
207 |     if (!voice && params.voiceId) {
208 |       issues.push(`Unknown voice ID: ${params.voiceId}`);
209 |     }
210 | 
211 |     // Check emotion compatibility (Speech 2.6 models support emotions)
212 |     if (params.emotion && params.emotion !== 'neutral') {
213 |       const emotionSupportedModels = ['speech-2.6-hd', 'speech-2.6-turbo'];
214 |       if (!emotionSupportedModels.includes(model)) {
215 |         issues.push(`Emotion parameter not supported by model ${model}`);
216 |       }
217 |     }
218 | 
219 |     return issues;
220 |   }
221 | 
222 | }
```

--------------------------------------------------------------------------------
/src/config/constants.ts:
--------------------------------------------------------------------------------

```typescript
  1 | // Type definitions
  2 | export interface ModelConfig {
  3 |   name: string;
  4 |   description: string;
  5 | }
  6 | 
  7 | export interface VoiceConfig {
  8 |   name: string;
  9 |   gender: 'male' | 'female' | 'other';
 10 |   language: 'zh' | 'en';
 11 | }
 12 | 
 13 | export interface RateLimit {
 14 |   rpm: number;
 15 |   burst: number;
 16 | }
 17 | 
 18 | export interface ApiConfig {
 19 |   BASE_URL: string;
 20 |   ENDPOINTS: {
 21 |     IMAGE_GENERATION: string;
 22 |     TEXT_TO_SPEECH: string;
 23 |   };
 24 |   HEADERS: {
 25 |     'MM-API-Source': string;
 26 |   };
 27 |   TIMEOUT: number;
 28 | }
 29 | 
 30 | export interface ImageConstraints {
 31 |   PROMPT_MAX_LENGTH: number;
 32 |   MAX_IMAGES: number;
 33 |   MIN_DIMENSION: number;
 34 |   MAX_DIMENSION: number;
 35 |   DIMENSION_STEP: number;
 36 |   ASPECT_RATIOS: readonly string[];
 37 |   STYLE_TYPES: readonly string[];
 38 |   RESPONSE_FORMATS: readonly string[];
 39 |   SUBJECT_TYPES: readonly string[];
 40 |   STYLE_WEIGHT_MIN: number;
 41 |   STYLE_WEIGHT_MAX: number;
 42 | }
 43 | 
 44 | export interface TTSConstraints {
 45 |   TEXT_MAX_LENGTH: number;
 46 |   SPEED_MIN: number;
 47 |   SPEED_MAX: number;
 48 |   VOLUME_MIN: number;
 49 |   VOLUME_MAX: number;
 50 |   PITCH_MIN: number;
 51 |   PITCH_MAX: number;
 52 |   EMOTIONS: readonly string[];
 53 |   FORMATS: readonly string[];
 54 |   SAMPLE_RATES: readonly number[];
 55 |   BITRATES: readonly number[];
 56 |   VOICE_MODIFY_PITCH_MIN: number;
 57 |   VOICE_MODIFY_PITCH_MAX: number;
 58 |   VOICE_MODIFY_INTENSITY_MIN: number;
 59 |   VOICE_MODIFY_INTENSITY_MAX: number;
 60 |   VOICE_MODIFY_TIMBRE_MIN: number;
 61 |   VOICE_MODIFY_TIMBRE_MAX: number;
 62 |   SOUND_EFFECTS: readonly string[];
 63 | }
 64 | 
 65 | export interface ImageDefaults {
 66 |   model: string;
 67 |   aspectRatio: string;
 68 |   n: number;
 69 |   promptOptimizer: boolean;
 70 |   responseFormat: string;
 71 |   styleWeight: number;
 72 | }
 73 | 
 74 | export interface TTSDefaults {
 75 |   model: string;
 76 |   voiceId: string;
 77 |   speed: number;
 78 |   volume: number;
 79 |   pitch: number;
 80 |   emotion: string;
 81 |   format: string;
 82 |   sampleRate: number;
 83 |   bitrate: number;
 84 |   channel: number;
 85 | }
 86 | 
 87 | // API Configuration
 88 | export const API_CONFIG: ApiConfig = {
 89 |   BASE_URL: 'https://api.minimaxi.com/v1',
 90 |   ENDPOINTS: {
 91 |     IMAGE_GENERATION: '/image_generation',
 92 |     TEXT_TO_SPEECH: '/t2a_v2'
 93 |   },
 94 |   HEADERS: {
 95 |     'MM-API-Source': 'mcp-tools'
 96 |   },
 97 |   TIMEOUT: 30000
 98 | } as const;
 99 | 
100 | // Rate Limiting Configuration
101 | export const RATE_LIMITS: Record<'IMAGE' | 'TTS', RateLimit> = {
102 |   IMAGE: { rpm: 10, burst: 3 },
103 |   TTS: { rpm: 20, burst: 5 }
104 | } as const;
105 | 
106 | // Model Configurations
107 | export const MODELS: Record<'IMAGE' | 'TTS', Record<string, ModelConfig>> = {
108 |   IMAGE: {
109 |     'image-01': { name: 'image-01', description: 'Standard image generation' },
110 |     'image-01-live': { name: 'image-01-live', description: 'Live image generation' }
111 |   },
112 |   TTS: {
113 |     'speech-2.6-hd': { name: 'speech-2.6-hd', description: 'Ultra-low latency, intelligent parsing, and enhanced naturalness' },
114 |     'speech-2.6-turbo': { name: 'speech-2.6-turbo', description: 'Faster, more affordable, ideal for voice agents with 40 languages support' }
115 |   }
116 | } as const;
117 | 
118 | // Voice Configurations
119 | export const VOICES: Record<string, VoiceConfig> = {
120 |   // Basic Chinese voices
121 |   'male-qn-qingse': { name: '青涩青年音色', gender: 'male', language: 'zh' },
122 |   'male-qn-jingying': { name: '精英青年音色', gender: 'male', language: 'zh' },
123 |   'male-qn-badao': { name: '霸道青年音色', gender: 'male', language: 'zh' },
124 |   'male-qn-daxuesheng': { name: '青年大学生音色', gender: 'male', language: 'zh' },
125 |   'female-shaonv': { name: '少女音色', gender: 'female', language: 'zh' },
126 |   'female-yujie': { name: '御姐音色', gender: 'female', language: 'zh' },
127 |   'female-chengshu': { name: '成熟女性音色', gender: 'female', language: 'zh' },
128 |   'female-tianmei': { name: '甜美女性音色', gender: 'female', language: 'zh' },
129 |   
130 |   // Professional voices
131 |   'presenter_male': { name: '男性主持人', gender: 'male', language: 'zh' },
132 |   'presenter_female': { name: '女性主持人', gender: 'female', language: 'zh' },
133 |   'audiobook_male_1': { name: '男性有声书1', gender: 'male', language: 'zh' },
134 |   'audiobook_male_2': { name: '男性有声书2', gender: 'male', language: 'zh' },
135 |   'audiobook_female_1': { name: '女性有声书1', gender: 'female', language: 'zh' },
136 |   'audiobook_female_2': { name: '女性有声书2', gender: 'female', language: 'zh' },
137 |   
138 |   // Beta voices
139 |   'male-qn-qingse-jingpin': { name: '青涩青年音色-beta', gender: 'male', language: 'zh' },
140 |   'male-qn-jingying-jingpin': { name: '精英青年音色-beta', gender: 'male', language: 'zh' },
141 |   'male-qn-badao-jingpin': { name: '霸道青年音色-beta', gender: 'male', language: 'zh' },
142 |   'male-qn-daxuesheng-jingpin': { name: '青年大学生音色-beta', gender: 'male', language: 'zh' },
143 |   'female-shaonv-jingpin': { name: '少女音色-beta', gender: 'female', language: 'zh' },
144 |   'female-yujie-jingpin': { name: '御姐音色-beta', gender: 'female', language: 'zh' },
145 |   'female-chengshu-jingpin': { name: '成熟女性音色-beta', gender: 'female', language: 'zh' },
146 |   'female-tianmei-jingpin': { name: '甜美女性音色-beta', gender: 'female', language: 'zh' },
147 |   
148 |   // Children voices
149 |   'clever_boy': { name: '聪明男童', gender: 'male', language: 'zh' },
150 |   'cute_boy': { name: '可爱男童', gender: 'male', language: 'zh' },
151 |   'lovely_girl': { name: '萌萌女童', gender: 'female', language: 'zh' },
152 |   'cartoon_pig': { name: '卡通猪小琪', gender: 'other', language: 'zh' },
153 |   
154 |   // Character voices
155 |   'bingjiao_didi': { name: '病娇弟弟', gender: 'male', language: 'zh' },
156 |   'junlang_nanyou': { name: '俊朗男友', gender: 'male', language: 'zh' },
157 |   'chunzhen_xuedi': { name: '纯真学弟', gender: 'male', language: 'zh' },
158 |   'lengdan_xiongzhang': { name: '冷淡学长', gender: 'male', language: 'zh' },
159 |   'badao_shaoye': { name: '霸道少爷', gender: 'male', language: 'zh' },
160 |   'tianxin_xiaoling': { name: '甜心小玲', gender: 'female', language: 'zh' },
161 |   'qiaopi_mengmei': { name: '俏皮萌妹', gender: 'female', language: 'zh' },
162 |   'wumei_yujie': { name: '妩媚御姐', gender: 'female', language: 'zh' },
163 |   'diadia_xuemei': { name: '嗲嗲学妹', gender: 'female', language: 'zh' },
164 |   'danya_xuejie': { name: '淡雅学姐', gender: 'female', language: 'zh' },
165 |   
166 |   // English voices
167 |   'Santa_Claus': { name: 'Santa Claus', gender: 'male', language: 'en' },
168 |   'Grinch': { name: 'Grinch', gender: 'male', language: 'en' },
169 |   'Rudolph': { name: 'Rudolph', gender: 'other', language: 'en' },
170 |   'Arnold': { name: 'Arnold', gender: 'male', language: 'en' },
171 |   'Charming_Santa': { name: 'Charming Santa', gender: 'male', language: 'en' },
172 |   'Charming_Lady': { name: 'Charming Lady', gender: 'female', language: 'en' },
173 |   'Sweet_Girl': { name: 'Sweet Girl', gender: 'female', language: 'en' },
174 |   'Cute_Elf': { name: 'Cute Elf', gender: 'other', language: 'en' },
175 |   'Attractive_Girl': { name: 'Attractive Girl', gender: 'female', language: 'en' },
176 |   'Serene_Woman': { name: 'Serene Woman', gender: 'female', language: 'en' }
177 | } as const;
178 | 
179 | // Parameter Constraints
180 | export const CONSTRAINTS = {
181 |   IMAGE: {
182 |     PROMPT_MAX_LENGTH: 1500,
183 |     MAX_IMAGES: 9,
184 |     MIN_DIMENSION: 512,
185 |     MAX_DIMENSION: 2048,
186 |     DIMENSION_STEP: 8,
187 |     ASPECT_RATIOS: ["1:1", "16:9", "4:3", "3:2", "2:3", "3:4", "9:16", "21:9"] as const,
188 |     STYLE_TYPES: ["漫画", "元气", "中世纪", "水彩"] as const,
189 |     RESPONSE_FORMATS: ["url", "base64"] as const,
190 |     SUBJECT_TYPES: ["character"] as const,
191 |     STYLE_WEIGHT_MIN: 0.01,
192 |     STYLE_WEIGHT_MAX: 1
193 |   },
194 |   TTS: {
195 |     TEXT_MAX_LENGTH: 10000,
196 |     SPEED_MIN: 0.5,
197 |     SPEED_MAX: 2.0,
198 |     VOLUME_MIN: 0.1,
199 |     VOLUME_MAX: 10.0,
200 |     PITCH_MIN: -12,
201 |     PITCH_MAX: 12,
202 |     EMOTIONS: ["neutral", "happy", "sad", "angry", "fearful", "disgusted", "surprised"] as const,
203 |     FORMATS: ["mp3", "wav", "flac", "pcm"] as const,
204 |     SAMPLE_RATES: ["8000", "16000", "22050", "24000", "32000", "44100"] as const,
205 |     BITRATES: ["64000", "96000", "128000", "160000", "192000", "224000", "256000", "320000"] as const,
206 |     VOICE_MODIFY_PITCH_MIN: -100,
207 |     VOICE_MODIFY_PITCH_MAX: 100,
208 |     VOICE_MODIFY_INTENSITY_MIN: -100,
209 |     VOICE_MODIFY_INTENSITY_MAX: 100,
210 |     VOICE_MODIFY_TIMBRE_MIN: -100,
211 |     VOICE_MODIFY_TIMBRE_MAX: 100,
212 |     SOUND_EFFECTS: ["spacious_echo", "auditorium_echo", "lofi_telephone", "robotic"] as const
213 |   }
214 | } as const;
215 | 
216 | // Default Values
217 | export const DEFAULTS = {
218 |   IMAGE: {
219 |     model: 'image-01',
220 |     aspectRatio: '1:1',
221 |     n: 1,
222 |     promptOptimizer: true,
223 |     responseFormat: 'url',
224 |     styleWeight: 0.8
225 |   },
226 |   TTS: {
227 |     model: 'speech-2.6-hd',
228 |     voiceId: 'female-shaonv',
229 |     speed: 1.0,
230 |     volume: 1.0,
231 |     pitch: 0,
232 |     emotion: 'neutral',
233 |     format: 'mp3',
234 |     sampleRate: "32000",
235 |     bitrate: "128000",
236 |     channel: 1
237 |   }
238 | } as const;
239 | 
240 | // Type exports for use in other modules
241 | export type ImageModel = keyof typeof MODELS.IMAGE;
242 | export type TTSModel = keyof typeof MODELS.TTS;
243 | export type VoiceId = keyof typeof VOICES;
244 | export type AspectRatio = typeof CONSTRAINTS.IMAGE.ASPECT_RATIOS[number];
245 | export type StyleType = typeof CONSTRAINTS.IMAGE.STYLE_TYPES[number];
246 | export type ResponseFormat = typeof CONSTRAINTS.IMAGE.RESPONSE_FORMATS[number];
247 | export type SubjectType = typeof CONSTRAINTS.IMAGE.SUBJECT_TYPES[number];
248 | export type Emotion = typeof CONSTRAINTS.TTS.EMOTIONS[number];
249 | export type AudioFormat = typeof CONSTRAINTS.TTS.FORMATS[number];
250 | export type SampleRate = typeof CONSTRAINTS.TTS.SAMPLE_RATES[number];
251 | export type Bitrate = typeof CONSTRAINTS.TTS.BITRATES[number];
252 | export type SoundEffect = typeof CONSTRAINTS.TTS.SOUND_EFFECTS[number];
```

--------------------------------------------------------------------------------
/src/config/schemas.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { z } from 'zod';
  2 | import { 
  3 |   CONSTRAINTS, 
  4 |   VOICES,
  5 |   type VoiceId,
  6 |   type AspectRatio,
  7 |   type StyleType,
  8 |   type Emotion,
  9 |   type AudioFormat,
 10 |   type SampleRate,
 11 |   type Bitrate,
 12 |   type SoundEffect
 13 | } from './constants.js';
 14 | 
 15 | // Base schemas
 16 | const filePathSchema = z.string().min(1, 'File path is required');
 17 | const positiveIntSchema = z.number().int().positive();
 18 | 
 19 | // Helper functions for generating descriptions
 20 | const getSoundEffectsDescription = () => {
 21 |   const descriptions = {
 22 |     'spacious_echo': 'spacious_echo (空旷回音)',
 23 |     'auditorium_echo': 'auditorium_echo (礼堂广播)',
 24 |     'lofi_telephone': 'lofi_telephone (电话失真)',
 25 |     'robotic': 'robotic (机械音)'
 26 |   };
 27 |   return `Sound effects. Options: ${CONSTRAINTS.TTS.SOUND_EFFECTS.map(effect => descriptions[effect] || effect).join(', ')}. Only one sound effect can be used per request`;
 28 | };
 29 | 
 30 | // Image generation schema
 31 | export const imageGenerationSchema = z.object({
 32 |   prompt: z.string()
 33 |     .min(1, 'Prompt is required')
 34 |     .max(CONSTRAINTS.IMAGE.PROMPT_MAX_LENGTH, `Prompt must not exceed ${CONSTRAINTS.IMAGE.PROMPT_MAX_LENGTH} characters`),
 35 |   
 36 |   outputFile: filePathSchema.describe('Absolute path for generated image'),
 37 |   
 38 |   
 39 |   aspectRatio: z.enum(CONSTRAINTS.IMAGE.ASPECT_RATIOS as readonly [AspectRatio, ...AspectRatio[]])
 40 |     .default('1:1' as AspectRatio)
 41 |     .describe(`Aspect ratio for the image. Options: ${CONSTRAINTS.IMAGE.ASPECT_RATIOS.join(', ')}`),
 42 |     
 43 |   customSize: z.object({
 44 |     width: z.number()
 45 |       .min(CONSTRAINTS.IMAGE.MIN_DIMENSION)
 46 |       .max(CONSTRAINTS.IMAGE.MAX_DIMENSION)
 47 |       .multipleOf(CONSTRAINTS.IMAGE.DIMENSION_STEP),
 48 |     height: z.number()
 49 |       .min(CONSTRAINTS.IMAGE.MIN_DIMENSION)
 50 |       .max(CONSTRAINTS.IMAGE.MAX_DIMENSION)
 51 |       .multipleOf(CONSTRAINTS.IMAGE.DIMENSION_STEP)
 52 |   }).optional().describe('Custom image dimensions (width x height in pixels). Range: 512-2048, must be multiples of 8. Total resolution should stay under 2M pixels. Only supported with image-01 model (cannot be used with style parameter). When both customSize and aspectRatio are set, aspectRatio takes precedence'),
 53 |   
 54 |     
 55 |   seed: positiveIntSchema.optional().describe('Random seed for reproducible results'),
 56 |   
 57 |   subjectReference: z.string().optional().describe('File path to a portrait image for maintaining facial characteristics in generated images. Only supported with image-01 model (cannot be used with style parameter). Provide a clear frontal face photo for best results. Supports local file paths and URLs. Max 10MB, formats: jpg, jpeg, png'),
 58 |   
 59 |   style: z.object({
 60 |     style_type: z.enum(CONSTRAINTS.IMAGE.STYLE_TYPES as readonly [StyleType, ...StyleType[]])
 61 |       .describe(`Art style type. Options: ${CONSTRAINTS.IMAGE.STYLE_TYPES.join(', ')}`),
 62 |     style_weight: z.number()
 63 |       .min(CONSTRAINTS.IMAGE.STYLE_WEIGHT_MIN, 'Style weight must be greater than 0')
 64 |       .max(CONSTRAINTS.IMAGE.STYLE_WEIGHT_MAX, 'Style weight must not exceed 1')
 65 |       .default(0.8)
 66 |       .describe('Style control weight (0-1]. Higher values apply stronger style effects. Default: 0.8')
 67 |   }).optional().describe('Art style control settings. Uses image-01-live model which does not support customSize or subjectReference parameters. Cannot be combined with customSize or subjectReference'),
 68 |   
 69 | });
 70 | 
 71 | // Text-to-speech schema
 72 | export const textToSpeechSchema = z.object({
 73 |   text: z.string()
 74 |     .min(1, 'Text is required')
 75 |     .max(CONSTRAINTS.TTS.TEXT_MAX_LENGTH, `Text to convert to speech. Max ${CONSTRAINTS.TTS.TEXT_MAX_LENGTH} characters. Use newlines for paragraph breaks. For custom pauses, insert <#x#> where x is seconds (0.01-99.99, max 2 decimals). Pause markers must be between pronounceable text and cannot be consecutive`),
 76 |     
 77 |   outputFile: filePathSchema.describe('Absolute path for audio file'),
 78 |   
 79 |   highQuality: z.boolean()
 80 |     .default(false)
 81 |     .describe('Use high-quality model (speech-02-hd) for audiobooks/premium content. Default: false (uses faster speech-02-turbo)'),
 82 |     
 83 |   voiceId: z.enum(Object.keys(VOICES) as [VoiceId, ...VoiceId[]])
 84 |     .default('female-shaonv' as VoiceId)
 85 |     .describe(`Voice ID for speech generation. Available voices: ${Object.keys(VOICES).map(id => `${id} (${VOICES[id as VoiceId]?.name || id})`).join(', ')}`),
 86 |     
 87 |   speed: z.number()
 88 |     .min(CONSTRAINTS.TTS.SPEED_MIN)
 89 |     .max(CONSTRAINTS.TTS.SPEED_MAX)
 90 |     .default(1.0)
 91 |     .describe(`Speech speed multiplier (${CONSTRAINTS.TTS.SPEED_MIN}-${CONSTRAINTS.TTS.SPEED_MAX}). Higher values = faster speech`),
 92 |     
 93 |   volume: z.number()
 94 |     .min(CONSTRAINTS.TTS.VOLUME_MIN)
 95 |     .max(CONSTRAINTS.TTS.VOLUME_MAX)
 96 |     .default(1.0)
 97 |     .describe(`Audio volume level (${CONSTRAINTS.TTS.VOLUME_MIN}-${CONSTRAINTS.TTS.VOLUME_MAX}). Higher values = louder audio`),
 98 |     
 99 |   pitch: z.number()
100 |     .min(CONSTRAINTS.TTS.PITCH_MIN)
101 |     .max(CONSTRAINTS.TTS.PITCH_MAX)
102 |     .default(0)
103 |     .describe(`Pitch adjustment in semitones (${CONSTRAINTS.TTS.PITCH_MIN} to ${CONSTRAINTS.TTS.PITCH_MAX}). Negative = lower pitch, Positive = higher pitch`),
104 |     
105 |   emotion: z.enum(CONSTRAINTS.TTS.EMOTIONS as readonly [Emotion, ...Emotion[]])
106 |     .default('neutral' as Emotion)
107 |     .describe(`Emotional tone of the speech. Options: ${CONSTRAINTS.TTS.EMOTIONS.join(', ')}`),
108 |     
109 |   format: z.enum(CONSTRAINTS.TTS.FORMATS as readonly [AudioFormat, ...AudioFormat[]])
110 |     .default('mp3' as AudioFormat)
111 |     .describe(`Output audio format. Options: ${CONSTRAINTS.TTS.FORMATS.join(', ')}`),
112 |     
113 |   sampleRate: z.enum(CONSTRAINTS.TTS.SAMPLE_RATES as readonly [SampleRate, ...SampleRate[]])
114 |     .default("32000" as SampleRate)
115 |     .describe(`Audio sample rate in Hz. Options: ${CONSTRAINTS.TTS.SAMPLE_RATES.join(', ')}`),
116 |     
117 |   bitrate: z.enum(CONSTRAINTS.TTS.BITRATES as readonly [Bitrate, ...Bitrate[]])
118 |     .default("128000" as Bitrate)  
119 |     .describe(`Audio bitrate in bps. Options: ${CONSTRAINTS.TTS.BITRATES.join(', ')}`),
120 |     
121 |   languageBoost: z.string().default('auto').describe('Enhance recognition for specific languages/dialects. Options: 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, Bulgarian, Danish, Hebrew, Malay, Persian, Slovak, Swedish, Croatian, Filipino, Hungarian, Norwegian, Slovenian, Catalan, Nynorsk, Tamil, Afrikaans, auto. Use "auto" for automatic detection'),
122 |   
123 |   intensity: z.number()
124 |     .int()
125 |     .min(CONSTRAINTS.TTS.VOICE_MODIFY_INTENSITY_MIN)
126 |     .max(CONSTRAINTS.TTS.VOICE_MODIFY_INTENSITY_MAX)
127 |     .optional()
128 |     .describe('Voice intensity adjustment (-100 to 100). Values closer to -100 make voice more robust, closer to 100 make voice softer'),
129 |     
130 |   timbre: z.number()
131 |     .int()
132 |     .min(CONSTRAINTS.TTS.VOICE_MODIFY_TIMBRE_MIN)
133 |     .max(CONSTRAINTS.TTS.VOICE_MODIFY_TIMBRE_MAX)
134 |     .optional()
135 |     .describe('Voice timbre adjustment (-100 to 100). Values closer to -100 make voice more mellow, closer to 100 make voice more crisp'),
136 |     
137 |   sound_effects: z.enum(CONSTRAINTS.TTS.SOUND_EFFECTS as readonly [SoundEffect, ...SoundEffect[]])
138 |     .optional()
139 |     .describe(getSoundEffectsDescription())
140 | });
141 | 
142 | // Task barrier schema
143 | export const taskBarrierSchema = z.object({});
144 | 
145 | // Type definitions for parsed schemas
146 | export type ImageGenerationParams = z.infer<typeof imageGenerationSchema>;
147 | export type TextToSpeechParams = z.infer<typeof textToSpeechSchema>;
148 | export type TaskBarrierParams = z.infer<typeof taskBarrierSchema>;
149 | 
150 | // MCP Tool Schemas (for registerTool API)
151 | export const imageGenerationToolSchema = {
152 |   type: "object",
153 |   properties: {
154 |       prompt: {
155 |         type: "string",
156 |         description: `Image generation prompt (max ${CONSTRAINTS.IMAGE.PROMPT_MAX_LENGTH} characters)`,
157 |         maxLength: CONSTRAINTS.IMAGE.PROMPT_MAX_LENGTH
158 |       },
159 |       outputFile: {
160 |         type: "string",
161 |         description: "Absolute path for generated image file"
162 |       },
163 |       aspectRatio: {
164 |         type: "string",
165 |         enum: [...CONSTRAINTS.IMAGE.ASPECT_RATIOS],
166 |         default: "1:1",
167 |         description: `Aspect ratio for the image. Options: ${CONSTRAINTS.IMAGE.ASPECT_RATIOS.join(', ')}`
168 |       },
169 |       customSize: {
170 |         type: "object",
171 |         properties: {
172 |           width: { type: "number", minimum: CONSTRAINTS.IMAGE.MIN_DIMENSION, maximum: CONSTRAINTS.IMAGE.MAX_DIMENSION, multipleOf: CONSTRAINTS.IMAGE.DIMENSION_STEP },
173 |           height: { type: "number", minimum: CONSTRAINTS.IMAGE.MIN_DIMENSION, maximum: CONSTRAINTS.IMAGE.MAX_DIMENSION, multipleOf: CONSTRAINTS.IMAGE.DIMENSION_STEP }
174 |         },
175 |         required: ["width", "height"],
176 |         description: "Custom image dimensions (width x height in pixels). Range: 512-2048, must be multiples of 8. Total resolution should stay under 2M pixels. Only supported with image-01 model (cannot be used with style parameter). When both customSize and aspectRatio are set, aspectRatio takes precedence"
177 |       },
178 |       seed: {
179 |         type: "number",
180 |         description: "Random seed for reproducible results"
181 |       },
182 |       subjectReference: {
183 |         type: "string",
184 |         description: "File path to a portrait image for maintaining facial characteristics in generated images. Only supported with image-01 model (cannot be used with style parameter). Provide a clear frontal face photo for best results. Supports local file paths and URLs. Max 10MB, formats: jpg, jpeg, png"
185 |       },
186 |       style: {
187 |         type: "object",
188 |         properties: {
189 |           style_type: { 
190 |             type: "string", 
191 |             enum: [...CONSTRAINTS.IMAGE.STYLE_TYPES], 
192 |             description: `Art style type. Options: ${CONSTRAINTS.IMAGE.STYLE_TYPES.join(', ')}` 
193 |           },
194 |           style_weight: { 
195 |             type: "number", 
196 |             exclusiveMinimum: 0, 
197 |             maximum: CONSTRAINTS.IMAGE.STYLE_WEIGHT_MAX, 
198 |             default: 0.8, 
199 |             description: "Style control weight (0-1]. Higher values apply stronger style effects. Default: 0.8" 
200 |           }
201 |         },
202 |         required: ["style_type"],
203 |         description: "Art style control settings. Uses image-01-live model which does not support customSize or subjectReference parameters. Cannot be combined with customSize or subjectReference"
204 |       }
205 |     },
206 |   required: ["prompt", "outputFile"]
207 | } as const;
208 | 
209 | export const textToSpeechToolSchema = {
210 |   type: "object",
211 |   properties: {
212 |       text: {
213 |         type: "string",
214 |         description: `Text to convert to speech. Max ${CONSTRAINTS.TTS.TEXT_MAX_LENGTH} characters. Use newlines for paragraph breaks. For custom pauses, insert <#x#> where x is seconds (0.01-99.99, max 2 decimals). Pause markers must be between pronounceable text and cannot be consecutive`,
215 |         maxLength: CONSTRAINTS.TTS.TEXT_MAX_LENGTH,
216 |         minLength: 1
217 |       },
218 |       outputFile: {
219 |         type: "string",
220 |         description: "Absolute path for audio file"
221 |       },
222 |       highQuality: {
223 |         type: "boolean",
224 |         default: false,
225 |         description: "Use high-quality model (speech-02-hd) for audiobooks/premium content. Default: false (uses faster speech-02-turbo)"
226 |       },
227 |       voiceId: {
228 |         type: "string",
229 |         enum: Object.keys(VOICES),
230 |         default: "female-shaonv",
231 |         description: `Voice ID for speech generation. Available voices: ${Object.keys(VOICES).map(id => `${id} (${VOICES[id as VoiceId]?.name || id})`).join(', ')}`
232 |       },
233 |       speed: {
234 |         type: "number",
235 |         minimum: CONSTRAINTS.TTS.SPEED_MIN,
236 |         maximum: CONSTRAINTS.TTS.SPEED_MAX,
237 |         default: 1.0,
238 |         description: `Speech speed multiplier (${CONSTRAINTS.TTS.SPEED_MIN}-${CONSTRAINTS.TTS.SPEED_MAX}). Higher values = faster speech`
239 |       },
240 |       volume: {
241 |         type: "number",
242 |         minimum: CONSTRAINTS.TTS.VOLUME_MIN,
243 |         maximum: CONSTRAINTS.TTS.VOLUME_MAX,
244 |         default: 1.0,
245 |         description: `Audio volume level (${CONSTRAINTS.TTS.VOLUME_MIN}-${CONSTRAINTS.TTS.VOLUME_MAX}). Higher values = louder audio`
246 |       },
247 |       pitch: {
248 |         type: "number",
249 |         minimum: CONSTRAINTS.TTS.PITCH_MIN,
250 |         maximum: CONSTRAINTS.TTS.PITCH_MAX,
251 |         default: 0,
252 |         description: `Pitch adjustment in semitones (${CONSTRAINTS.TTS.PITCH_MIN} to ${CONSTRAINTS.TTS.PITCH_MAX}). Negative = lower pitch, Positive = higher pitch`
253 |       },
254 |       emotion: {
255 |         type: "string",
256 |         enum: [...CONSTRAINTS.TTS.EMOTIONS],
257 |         default: "neutral",
258 |         description: `Emotional tone of the speech. Options: ${CONSTRAINTS.TTS.EMOTIONS.join(', ')}`
259 |       },
260 |       format: {
261 |         type: "string",
262 |         enum: [...CONSTRAINTS.TTS.FORMATS],
263 |         default: "mp3",
264 |         description: `Output audio format. Options: ${CONSTRAINTS.TTS.FORMATS.join(', ')}`
265 |       },
266 |       sampleRate: {
267 |         type: "string",
268 |         enum: [...CONSTRAINTS.TTS.SAMPLE_RATES],
269 |         default: "32000",
270 |         description: `Audio sample rate in Hz. Options: ${CONSTRAINTS.TTS.SAMPLE_RATES.join(', ')}`
271 |       },
272 |       bitrate: {
273 |         type: "string",
274 |         enum: [...CONSTRAINTS.TTS.BITRATES],
275 |         default: "128000",
276 |         description: `Audio bitrate in bps. Options: ${CONSTRAINTS.TTS.BITRATES.join(', ')}`
277 |       },
278 |       languageBoost: {
279 |         type: "string",
280 |         default: "auto",
281 |         description: "Enhance recognition for specific languages/dialects. Options: 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, Bulgarian, Danish, Hebrew, Malay, Persian, Slovak, Swedish, Croatian, Filipino, Hungarian, Norwegian, Slovenian, Catalan, Nynorsk, Tamil, Afrikaans, auto. Use 'auto' for automatic detection"
282 |       },
283 |       intensity: {
284 |         type: "number", 
285 |         minimum: CONSTRAINTS.TTS.VOICE_MODIFY_INTENSITY_MIN,
286 |         maximum: CONSTRAINTS.TTS.VOICE_MODIFY_INTENSITY_MAX,
287 |         description: "Voice intensity adjustment (-100 to 100). Values closer to -100 make voice more robust, closer to 100 make voice softer"
288 |       },
289 |       timbre: {
290 |         type: "number",
291 |         minimum: CONSTRAINTS.TTS.VOICE_MODIFY_TIMBRE_MIN,
292 |         maximum: CONSTRAINTS.TTS.VOICE_MODIFY_TIMBRE_MAX,
293 |         description: "Voice timbre adjustment (-100 to 100). Values closer to -100 make voice more mellow, closer to 100 make voice more crisp"
294 |       },
295 |       sound_effects: {
296 |         type: "string",
297 |         enum: [...CONSTRAINTS.TTS.SOUND_EFFECTS],
298 |         description: getSoundEffectsDescription()
299 |       }
300 |     },
301 |   required: ["text", "outputFile"]
302 | } as const;
303 | 
304 | export const taskBarrierToolSchema = {
305 |   type: "object",
306 |   properties: {}
307 | } as const;
308 | 
309 | // Validation helper functions
310 | export function validateImageParams(params: unknown): ImageGenerationParams {
311 |   try {
312 |     const parsed = imageGenerationSchema.parse(params);
313 |     
314 |     // Manual validation for incompatible parameter combinations
315 |     const hasStyle = !!parsed.style;
316 |     const hasCustomSize = !!parsed.customSize;
317 |     const hasSubjectReference = !!parsed.subjectReference;
318 |     
319 |     if (hasStyle && hasCustomSize) {
320 |       throw new Error('Style parameter (image-01-live model) cannot be combined with customSize (image-01 model feature)');
321 |     }
322 |     
323 |     if (hasStyle && hasSubjectReference) {
324 |       throw new Error('Style parameter (image-01-live model) cannot be combined with subjectReference (image-01 model feature)');
325 |     }
326 |     
327 |     return parsed;
328 |   } catch (error) {
329 |     if (error instanceof z.ZodError) {
330 |       const messages = error.errors.map(e => `${e.path.join('.')}: ${e.message}`);
331 |       throw new Error(`Validation failed: ${messages.join(', ')}`);
332 |     }
333 |     throw error;
334 |   }
335 | }
336 | 
337 | export function validateTTSParams(params: unknown): TextToSpeechParams {
338 |   try {
339 |     return textToSpeechSchema.parse(params);
340 |   } catch (error) {
341 |     if (error instanceof z.ZodError) {
342 |       const messages = error.errors.map(e => `${e.path.join('.')}: ${e.message}`);
343 |       throw new Error(`Validation failed: ${messages.join(', ')}`);
344 |     }
345 |     throw error;
346 |   }
347 | }
348 | 
349 | export function validateTaskBarrierParams(params: unknown): TaskBarrierParams {
350 |   try {
351 |     return taskBarrierSchema.parse(params);
352 |   } catch (error) {
353 |     if (error instanceof z.ZodError) {
354 |       const messages = error.errors.map(e => `${e.path.join('.')}: ${e.message}`);
355 |       throw new Error(`Validation failed: ${messages.join(', ')}`);
356 |     }
357 |     throw error;
358 |   }
359 | }
```