botnoi-voice-mcp 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,71 @@
1
+ # Botnoi Voice MCP 🇹🇭🗣️
2
+
3
+ A **Model Context Protocol (MCP)** server for Text-to-Speech generation (Thai, English, and other regional languages) via the **Botnoi Voice API**. Designed to empower AI assistants (e.g., Claude 3.5 Sonnet, Cursor) to seamlessly synthesize speech and discover available voices dynamically.
4
+
5
+ ## 🌟 Features
6
+ - 🎙️ **Generate Speech** (`generate_speech`): Allows the AI to convert text immediately to audio and download it as an `.mp3` file.
7
+ - 🔍 **List & Filter Voices** (`list_voices`): Smartly search and filter through the Botnoi Voice library. Examples: search for "Thai female teen voice" or "Male news anchor voice".
8
+
9
+ ---
10
+
11
+ ## 🚀 Installation
12
+
13
+ You can use the server directly via `npx` (no installation required) or install it globally:
14
+
15
+ ```bash
16
+ # Global installation
17
+ npm install -g botnoi-voice-mcp
18
+ ```
19
+
20
+ ---
21
+
22
+ ## 🔑 Configuration
23
+
24
+ A Botnoi Voice API Key is required. You can register and obtain one from the [Botnoi Voice Dashboard](https://voice.botnoi.ai/).
25
+ Set your API key as an environment variable named `BOTNOI_API_KEY`.
26
+
27
+ ### Example Configuration for Claude Desktop
28
+
29
+ Add the server to your Claude Desktop configuration file (e.g., on Windows: `%APPDATA%\Claude\claude_desktop_config.json`):
30
+
31
+ ```json
32
+ {
33
+ "mcpServers": {
34
+ "botnoi-voice": {
35
+ "command": "npx",
36
+ "args": ["-y", "botnoi-voice-mcp"],
37
+ "env": {
38
+ "BOTNOI_API_KEY": "YOUR_API_KEY_HERE"
39
+ }
40
+ }
41
+ }
42
+ }
43
+ ```
44
+
45
+ ---
46
+
47
+ ## 🛠️ Available Tools
48
+
49
+ ### `list_voices`
50
+ Discover and search the available voice models inside the Botnoi Voice library.
51
+ - `language` (optional): Filter by language (e.g., `'th'`, `'en'`, `'ja'`).
52
+ - `gender` (optional): Filter by gender (e.g., `'Female'`, `'Male'`).
53
+ - `age_style` (optional): Filter by age group (e.g., `'วัยเด็ก'` (Child), `'วัยรุ่น'` (Teen), `'วัยผู้ใหญ่'` (Adult)).
54
+
55
+ ### `generate_speech`
56
+ Convert specified text into a speech output.
57
+ - `text` (required): The text to be synthesized into speech.
58
+ - `speaker` (optional): The Voice ID (e.g., `"1"`). Defaults to a basic voice.
59
+ - `language` (optional): Language code (e.g., `"th"`).
60
+ - `speed` (optional): Speech speed ranging from 0.5 to 2.0.
61
+
62
+ ---
63
+
64
+ ## ⚠️ Important Note for Developers
65
+
66
+ > If you are a developer extending this MCP, note that `list_voices` currently reads directly from a static local JSON response file. **If you publish this to NPM**, please ensure the `.json` file is correctly bundled in your build output (`dist/`) and referenced via `__dirname`, or fallback to making a live HTTP `fetch` to ensure it works across all user environments!
67
+
68
+ ---
69
+
70
+ ## 📝 License
71
+ MIT
package/dist/index.js ADDED
@@ -0,0 +1,197 @@
1
+ #!/usr/bin/env node
2
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
5
+ import fs from "fs";
6
+ import path from "path";
7
+ const API_KEY = process.env.BOTNOI_API_KEY || "";
8
+ const BASE_URL = "https://api-voice.botnoi.ai/openapi/v1";
9
+ // ===== สร้าง Server =====
10
+ const server = new Server({ name: "botnoi-voice-mcp", version: "1.0.0" }, { capabilities: { tools: {} } });
11
+ // ===== กำหนด Tools =====
12
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
13
+ tools: [
14
+ {
15
+ name: "generate_speech",
16
+ description: "แปลงข้อความเป็นเสียงพูดด้วย Botnoi Voice API รองรับภาษาไทย, อังกฤษ และภาษาอาเซียน",
17
+ inputSchema: {
18
+ type: "object",
19
+ properties: {
20
+ text: {
21
+ type: "string",
22
+ description: "ข้อความที่ต้องการแปลงเป็นเสียง",
23
+ },
24
+ speaker: {
25
+ type: "string",
26
+ description: "ID ของเสียงที่ต้องการ (เช่น '1' สำหรับ Eva)",
27
+ default: "1",
28
+ },
29
+ language: {
30
+ type: "string",
31
+ description: "ภาษา เช่น 'th' (ไทย), 'en' (อังกฤษ)",
32
+ default: "th",
33
+ },
34
+ speed: {
35
+ type: "number",
36
+ description: "ความเร็วในการพูด (0.5 - 2.0)",
37
+ default: 1,
38
+ },
39
+ output_path: {
40
+ type: "string",
41
+ description: "path สำหรับบันทึกไฟล์ MP3 (optional)",
42
+ },
43
+ },
44
+ required: ["text"],
45
+ },
46
+ },
47
+ {
48
+ name: "list_voices",
49
+ description: "ค้นหาและดูรายการเสียงที่มีใน Botnoi Voice (เช่น หาเสียงผู้หญิง, เสียงเด็ก, เสียงภาษาไทย)",
50
+ inputSchema: {
51
+ type: "object",
52
+ properties: {
53
+ language: {
54
+ type: "string",
55
+ description: "กรองตามภาษา เช่น 'th', 'en'",
56
+ },
57
+ gender: {
58
+ type: "string",
59
+ description: "กรองตามเพศ เช่น 'ผู้หญิง', 'ผู้ชาย', 'Female', 'Male'",
60
+ },
61
+ age_style: {
62
+ type: "string",
63
+ description: "กรองตามช่วงวัย เช่น 'วัยเด็ก', 'วัยรุ่น', 'วัยผู้ใหญ่'",
64
+ }
65
+ },
66
+ },
67
+ },
68
+ ],
69
+ }));
70
+ // ===== Tool Handlers =====
71
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
72
+ const { name, arguments: args } = request.params;
73
+ if (!API_KEY) {
74
+ return {
75
+ content: [
76
+ {
77
+ type: "text",
78
+ text: "❌ กรุณาตั้งค่า BOTNOI_API_KEY environment variable",
79
+ },
80
+ ],
81
+ };
82
+ }
83
+ if (name === "generate_speech") {
84
+ const { text, speaker = "1", language = "th", speed = 1, output_path } = args;
85
+ try {
86
+ const response = await fetch(`${BASE_URL}/generate_audio`, {
87
+ method: "POST",
88
+ headers: {
89
+ "accept": "application/json",
90
+ "Content-Type": "application/json",
91
+ "botnoi-token": API_KEY,
92
+ },
93
+ body: JSON.stringify({
94
+ text,
95
+ speaker,
96
+ language,
97
+ speed,
98
+ volume: 1,
99
+ type_media: "mp3",
100
+ save_file: "true",
101
+ page: "user",
102
+ }),
103
+ });
104
+ if (!response.ok) {
105
+ const err = await response.text();
106
+ throw new Error(`API Error ${response.status}: ${err}`);
107
+ }
108
+ const data = await response.json();
109
+ const audioUrl = data.audio_url || data.url;
110
+ // ถ้ามี output_path ให้ download ไฟล์มาเก็บ
111
+ if (output_path && audioUrl) {
112
+ const audioRes = await fetch(audioUrl);
113
+ const buffer = await audioRes.arrayBuffer();
114
+ fs.writeFileSync(output_path, Buffer.from(buffer));
115
+ return {
116
+ content: [
117
+ {
118
+ type: "text",
119
+ text: `✅ สร้างเสียงสำเร็จ!\n📁 บันทึกที่: ${path.resolve(output_path)}\n🔗 URL: ${audioUrl}`,
120
+ },
121
+ ],
122
+ };
123
+ }
124
+ return {
125
+ content: [
126
+ {
127
+ type: "text",
128
+ text: `✅ สร้างเสียงสำเร็จ!\n🔗 URL: ${audioUrl}\n\nสามารถเล่นหรือดาวน์โหลดได้จาก URL ด้านบน`,
129
+ },
130
+ ],
131
+ };
132
+ }
133
+ catch (error) {
134
+ return {
135
+ content: [
136
+ {
137
+ type: "text",
138
+ text: `❌ เกิดข้อผิดพลาด: ${error.message}`,
139
+ },
140
+ ],
141
+ isError: true,
142
+ };
143
+ }
144
+ }
145
+ if (name === "list_voices") {
146
+ try {
147
+ const { language, gender, age_style } = args || {};
148
+ const filePath = path.resolve(process.cwd(), "response_1775701990226.json");
149
+ let data;
150
+ try {
151
+ const fileContent = fs.readFileSync(filePath, "utf-8");
152
+ data = JSON.parse(fileContent);
153
+ }
154
+ catch (err) {
155
+ throw new Error(`ไม่สามารถอ่านไฟล์ JSON ได้: ${err.message}`);
156
+ }
157
+ if (language) {
158
+ const langLower = String(language).toLowerCase();
159
+ data = data.filter((v) => v.language?.toLowerCase() === langLower || v.available_language?.includes(langLower));
160
+ }
161
+ if (gender) {
162
+ const genderLower = String(gender).toLowerCase();
163
+ data = data.filter((v) => v.gender === gender || v.eng_gender?.toLowerCase() === genderLower);
164
+ }
165
+ if (age_style) {
166
+ data = data.filter((v) => v.age_style === age_style || v.eng_age_style?.toLowerCase() === String(age_style).toLowerCase());
167
+ }
168
+ const conciseData = data.map((v) => ({
169
+ speaker_id: v.speaker_id,
170
+ name: v.thai_name || v.eng_name,
171
+ gender: v.gender,
172
+ age: v.age_style,
173
+ style: v.voice_style?.join(", ") || "",
174
+ speed: v.speed
175
+ }));
176
+ return {
177
+ content: [{ type: "text", text: JSON.stringify(conciseData, null, 2) }],
178
+ };
179
+ }
180
+ catch (error) {
181
+ return {
182
+ content: [
183
+ { type: "text", text: `❌ ${error.message}` },
184
+ ],
185
+ isError: true,
186
+ };
187
+ }
188
+ }
189
+ return {
190
+ content: [{ type: "text", text: `Tool "${name}" ไม่พบ` }],
191
+ isError: true,
192
+ };
193
+ });
194
+ // ===== Start Server =====
195
+ const transport = new StdioServerTransport();
196
+ await server.connect(transport);
197
+ console.error("Botnoi Voice MCP Server running...");