@coeiro-operator/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 +86 -0
- package/dist/server.d.ts +3 -0
- package/dist/server.js +833 -0
- package/package.json +59 -0
package/README.md
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# @coeiro-operator/mcp
|
|
2
|
+
|
|
3
|
+
COEIROINK音声合成のMCP(Model Context Protocol)サーバー
|
|
4
|
+
|
|
5
|
+
Claude Codeで音声対話機能を実現するMCPサーバーです。
|
|
6
|
+
|
|
7
|
+
## インストール
|
|
8
|
+
|
|
9
|
+
### 方法1: グローバルインストール(推奨)
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
# インストール
|
|
13
|
+
npm install -g @coeiro-operator/mcp
|
|
14
|
+
|
|
15
|
+
# MCPサーバーを登録
|
|
16
|
+
claude mcp add coeiro-operator
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
### 方法2: npxで直接実行
|
|
20
|
+
|
|
21
|
+
`claude_desktop_config.json`に直接追加:
|
|
22
|
+
|
|
23
|
+
```json
|
|
24
|
+
{
|
|
25
|
+
"mcpServers": {
|
|
26
|
+
"coeiro-operator": {
|
|
27
|
+
"command": "npx",
|
|
28
|
+
"args": ["-y", "@coeiro-operator/mcp"]
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
※ `npx @coeiro-operator/mcp`は内部的に`coeiro-operator`コマンドを実行します
|
|
35
|
+
|
|
36
|
+
## 提供される機能
|
|
37
|
+
|
|
38
|
+
### 音声合成
|
|
39
|
+
|
|
40
|
+
- `say` - テキストを音声で読み上げ
|
|
41
|
+
- `operator_assign` - オペレータ(音声キャラクター)の割り当て
|
|
42
|
+
- `operator_release` - オペレータの解放
|
|
43
|
+
- `operator_status` - 現在のオペレータ状態確認
|
|
44
|
+
- `operator_available` - 利用可能なオペレータ一覧
|
|
45
|
+
- `operator_styles` - 音声スタイル一覧
|
|
46
|
+
|
|
47
|
+
### ユーザー辞書
|
|
48
|
+
|
|
49
|
+
- `dictionary_register` - 単語の読み方を登録
|
|
50
|
+
|
|
51
|
+
### デバッグ
|
|
52
|
+
|
|
53
|
+
- `debug_logs` - デバッグログの取得
|
|
54
|
+
- `parallel_generation_control` - 並行生成制御
|
|
55
|
+
|
|
56
|
+
## 使用例(Claude Codeでの会話)
|
|
57
|
+
|
|
58
|
+
```
|
|
59
|
+
ユーザー: こんにちはと音声で言って
|
|
60
|
+
|
|
61
|
+
Claude Code: [say ツールを使用して「こんにちは」を音声出力]
|
|
62
|
+
|
|
63
|
+
ユーザー: つくよみちゃんに切り替えて
|
|
64
|
+
|
|
65
|
+
Claude Code: [operator_assign ツールでtsukuyomiに切り替え]
|
|
66
|
+
|
|
67
|
+
ユーザー: ひそひそ声でお疲れ様と言って
|
|
68
|
+
|
|
69
|
+
Claude Code: [say ツールでstyle: "ひそひそ"を指定して音声出力]
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
※ MCPツールはClaude Codeが自動的に判断して使用します
|
|
73
|
+
|
|
74
|
+
## 動作要件
|
|
75
|
+
|
|
76
|
+
- Node.js 18以上
|
|
77
|
+
- COEIROINK本体が起動済み(http://localhost:50032)
|
|
78
|
+
- Claude Code(MCPクライアント)
|
|
79
|
+
|
|
80
|
+
## 詳細
|
|
81
|
+
|
|
82
|
+
完全なドキュメントは [GitHub](https://github.com/otolab/coeiro-operator) を参照してください。
|
|
83
|
+
|
|
84
|
+
## ライセンス
|
|
85
|
+
|
|
86
|
+
MIT
|
package/dist/server.d.ts
ADDED
package/dist/server.js
ADDED
|
@@ -0,0 +1,833 @@
|
|
|
1
|
+
#!/usr/bin/env node --no-deprecation
|
|
2
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
import * as path from 'path';
|
|
6
|
+
import { SayCoeiroink } from '@coeiro-operator/audio';
|
|
7
|
+
import { ConfigManager, getConfigDir, OperatorManager, DictionaryService, TerminalBackground } from '@coeiro-operator/core';
|
|
8
|
+
import { logger, LoggerPresets } from '@coeiro-operator/common';
|
|
9
|
+
// コマンドライン引数の解析
|
|
10
|
+
const parseArguments = () => {
|
|
11
|
+
const args = process.argv.slice(2);
|
|
12
|
+
let isDebugMode = false;
|
|
13
|
+
let configPath;
|
|
14
|
+
for (let i = 0; i < args.length; i++) {
|
|
15
|
+
const arg = args[i];
|
|
16
|
+
if (arg === '--debug' || arg === '-d') {
|
|
17
|
+
isDebugMode = true;
|
|
18
|
+
}
|
|
19
|
+
else if (arg === '--config' || arg === '-c') {
|
|
20
|
+
configPath = args[i + 1];
|
|
21
|
+
i++; // 次の引数をスキップ
|
|
22
|
+
}
|
|
23
|
+
else if (arg.startsWith('--config=')) {
|
|
24
|
+
configPath = arg.split('=')[1];
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return { isDebugMode, configPath };
|
|
28
|
+
};
|
|
29
|
+
const { isDebugMode, configPath } = parseArguments();
|
|
30
|
+
// デバッグモードの場合は詳細ログ、そうでなければMCPサーバーモード
|
|
31
|
+
if (isDebugMode) {
|
|
32
|
+
LoggerPresets.mcpServerDebugWithAccumulation(); // デバッグモード:全レベル出力・蓄積
|
|
33
|
+
logger.info('DEBUG MODE: Verbose logging enabled (--debug flag detected)');
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
LoggerPresets.mcpServerWithAccumulation(); // 通常モード:info以上のみ蓄積
|
|
37
|
+
}
|
|
38
|
+
if (configPath) {
|
|
39
|
+
logger.info(`Using config file: ${configPath}`);
|
|
40
|
+
}
|
|
41
|
+
const server = new McpServer({
|
|
42
|
+
name: 'coeiro-operator',
|
|
43
|
+
version: '1.0.0',
|
|
44
|
+
}, {
|
|
45
|
+
capabilities: {
|
|
46
|
+
tools: {},
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
// top-level awaitを使用した同期的初期化
|
|
50
|
+
logger.info('Initializing COEIRO Operator services...');
|
|
51
|
+
// 環境変数のデバッグ出力
|
|
52
|
+
logger.debug('Environment variables check:', {
|
|
53
|
+
TERM_PROGRAM: process.env.TERM_PROGRAM,
|
|
54
|
+
ITERM_SESSION_ID: process.env.ITERM_SESSION_ID,
|
|
55
|
+
TERM_SESSION_ID: process.env.TERM_SESSION_ID,
|
|
56
|
+
NODE_ENV: process.env.NODE_ENV
|
|
57
|
+
});
|
|
58
|
+
let sayCoeiroink;
|
|
59
|
+
let operatorManager;
|
|
60
|
+
let dictionaryService;
|
|
61
|
+
let terminalBackground = null;
|
|
62
|
+
try {
|
|
63
|
+
const configDir = configPath ? path.dirname(configPath) : await getConfigDir();
|
|
64
|
+
const configManager = new ConfigManager(configDir);
|
|
65
|
+
await configManager.buildDynamicConfig();
|
|
66
|
+
// TerminalBackgroundを初期化
|
|
67
|
+
terminalBackground = new TerminalBackground(configManager);
|
|
68
|
+
sayCoeiroink = new SayCoeiroink(configManager);
|
|
69
|
+
logger.info('Initializing SayCoeiroink...');
|
|
70
|
+
await sayCoeiroink.initialize();
|
|
71
|
+
logger.info('Building dynamic config...');
|
|
72
|
+
await sayCoeiroink.buildDynamicConfig();
|
|
73
|
+
logger.info('Initializing OperatorManager...');
|
|
74
|
+
operatorManager = new OperatorManager();
|
|
75
|
+
await operatorManager.initialize();
|
|
76
|
+
logger.info('Initializing Dictionary...');
|
|
77
|
+
const config = await configManager.getFullConfig();
|
|
78
|
+
dictionaryService = new DictionaryService(config?.connection);
|
|
79
|
+
await dictionaryService.initialize();
|
|
80
|
+
logger.info('SayCoeiroink, OperatorManager and Dictionary initialized successfully');
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
logger.error('Failed to initialize services:', error.message);
|
|
84
|
+
logger.error('Error stack:', error.stack);
|
|
85
|
+
logger.warn('Using fallback configuration...');
|
|
86
|
+
// フォールバック設定で初期化
|
|
87
|
+
try {
|
|
88
|
+
const fallbackConfigDir = await getConfigDir();
|
|
89
|
+
const fallbackConfigManager = new ConfigManager(fallbackConfigDir);
|
|
90
|
+
await fallbackConfigManager.buildDynamicConfig();
|
|
91
|
+
// TerminalBackgroundを初期化
|
|
92
|
+
terminalBackground = new TerminalBackground(fallbackConfigManager);
|
|
93
|
+
sayCoeiroink = new SayCoeiroink(fallbackConfigManager);
|
|
94
|
+
await sayCoeiroink.initialize();
|
|
95
|
+
await sayCoeiroink.buildDynamicConfig();
|
|
96
|
+
operatorManager = new OperatorManager();
|
|
97
|
+
await operatorManager.initialize();
|
|
98
|
+
dictionaryService = new DictionaryService();
|
|
99
|
+
await dictionaryService.initialize();
|
|
100
|
+
logger.info('Fallback initialization completed');
|
|
101
|
+
}
|
|
102
|
+
catch (fallbackError) {
|
|
103
|
+
logger.error('Fallback initialization also failed:', fallbackError.message);
|
|
104
|
+
throw fallbackError;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
// Utility functions for operator assignment
|
|
108
|
+
function validateOperatorInput(operator) {
|
|
109
|
+
if (operator !== undefined && operator !== '' && operator !== null) {
|
|
110
|
+
if (/[\u3040-\u309F\u30A0-\u30FF\u4E00-\u9FAF]/.test(operator)) {
|
|
111
|
+
throw new Error('オペレータ名は英語表記で指定してください(例: tsukuyomi, alma)。日本語は使用できません。');
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
async function assignOperator(manager, operator, style) {
|
|
116
|
+
if (operator && operator !== '' && operator !== null) {
|
|
117
|
+
return await manager.assignSpecificOperator(operator, style);
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
return await manager.assignRandomOperator(style);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
function extractStyleInfo(character) {
|
|
124
|
+
return (character.speaker?.styles || []).map(style => ({
|
|
125
|
+
id: style.styleId.toString(),
|
|
126
|
+
name: style.styleName,
|
|
127
|
+
personality: character.personality,
|
|
128
|
+
speakingStyle: character.speakingStyle,
|
|
129
|
+
}));
|
|
130
|
+
}
|
|
131
|
+
function formatAssignmentResult(assignResult, availableStyles) {
|
|
132
|
+
let resultText = `${assignResult.characterName} (${assignResult.characterId}) をアサインしました。\n\n`;
|
|
133
|
+
if (assignResult.currentStyle) {
|
|
134
|
+
resultText += `📍 現在のスタイル: ${assignResult.currentStyle.styleName}\n`;
|
|
135
|
+
resultText += ` 性格: ${assignResult.currentStyle.personality}\n`;
|
|
136
|
+
resultText += ` 話し方: ${assignResult.currentStyle.speakingStyle}\n\n`;
|
|
137
|
+
}
|
|
138
|
+
if (availableStyles.length > 1) {
|
|
139
|
+
resultText += `🎭 利用可能なスタイル(切り替え可能):\n`;
|
|
140
|
+
availableStyles.forEach(style => {
|
|
141
|
+
const isCurrent = style.id === assignResult.currentStyle?.styleId;
|
|
142
|
+
const marker = isCurrent ? '→ ' : ' ';
|
|
143
|
+
resultText += `${marker}${style.id}: ${style.name}\n`;
|
|
144
|
+
resultText += ` 性格: ${style.personality}\n`;
|
|
145
|
+
resultText += ` 話し方: ${style.speakingStyle}\n`;
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
resultText += `ℹ️ このキャラクターは1つのスタイルのみ利用可能です。\n`;
|
|
150
|
+
}
|
|
151
|
+
if (assignResult.greeting) {
|
|
152
|
+
resultText += `\n💬 "${assignResult.greeting}"\n`;
|
|
153
|
+
}
|
|
154
|
+
return resultText;
|
|
155
|
+
}
|
|
156
|
+
// Utility functions for operator styles
|
|
157
|
+
async function getTargetCharacter(manager, characterId) {
|
|
158
|
+
if (characterId) {
|
|
159
|
+
try {
|
|
160
|
+
const character = await manager.getCharacterInfo(characterId);
|
|
161
|
+
if (!character) {
|
|
162
|
+
throw new Error(`キャラクター '${characterId}' が見つかりません`);
|
|
163
|
+
}
|
|
164
|
+
return { character, characterId };
|
|
165
|
+
}
|
|
166
|
+
catch (error) {
|
|
167
|
+
throw new Error(`キャラクター '${characterId}' が見つかりません`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
else {
|
|
171
|
+
const currentOperator = await manager.showCurrentOperator();
|
|
172
|
+
if (!currentOperator.characterId) {
|
|
173
|
+
throw new Error('現在オペレータが割り当てられていません。まず operator_assign を実行してください。');
|
|
174
|
+
}
|
|
175
|
+
const character = await manager.getCharacterInfo(currentOperator.characterId);
|
|
176
|
+
if (!character) {
|
|
177
|
+
throw new Error(`現在のオペレータ '${currentOperator.characterId}' のキャラクター情報が見つかりません`);
|
|
178
|
+
}
|
|
179
|
+
return { character, characterId: currentOperator.characterId };
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
function formatStylesResult(character, availableStyles) {
|
|
183
|
+
let resultText = `🎭 ${character.speaker?.speakerName || character.characterId} のスタイル情報\n\n`;
|
|
184
|
+
resultText += `📋 基本情報:\n`;
|
|
185
|
+
resultText += ` 性格: ${character.personality}\n`;
|
|
186
|
+
resultText += ` 話し方: ${character.speakingStyle}\n`;
|
|
187
|
+
resultText += ` デフォルトスタイル: ${character.defaultStyle}\n\n`;
|
|
188
|
+
if (availableStyles.length > 0) {
|
|
189
|
+
resultText += `🎨 利用可能なスタイル (${availableStyles.length}種類):\n`;
|
|
190
|
+
availableStyles.forEach((style, index) => {
|
|
191
|
+
const isDefault = style.id === character.defaultStyle;
|
|
192
|
+
const marker = isDefault ? '★ ' : `${index + 1}. `;
|
|
193
|
+
resultText += `${marker}${style.id}: ${style.name}\n`;
|
|
194
|
+
resultText += ` 性格: ${style.personality}\n`;
|
|
195
|
+
resultText += ` 話し方: ${style.speakingStyle}\n`;
|
|
196
|
+
if (isDefault) {
|
|
197
|
+
resultText += ` (デフォルトスタイル)\n`;
|
|
198
|
+
}
|
|
199
|
+
resultText += `\n`;
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
resultText += `⚠️ 利用可能なスタイルがありません。\n`;
|
|
204
|
+
}
|
|
205
|
+
return resultText;
|
|
206
|
+
}
|
|
207
|
+
// operator-manager操作ツール
|
|
208
|
+
server.registerTool('operator_assign', {
|
|
209
|
+
description: 'オペレータをランダム選択して割り当てます。アサイン後に現在のスタイルと利用可能な他のスタイル情報を表示します。スタイル切り替えはsayツールのstyleパラメータで日本語名を指定します(例: say({message: "テスト", style: "ひそひそ"}))。',
|
|
210
|
+
inputSchema: {
|
|
211
|
+
operator: z
|
|
212
|
+
.string()
|
|
213
|
+
.optional()
|
|
214
|
+
.describe("指定するオペレータ名(英語表記、例: 'tsukuyomi', 'alma'など。省略時または空文字列時はランダム選択。日本語表記は無効)"),
|
|
215
|
+
style: z
|
|
216
|
+
.string()
|
|
217
|
+
.optional()
|
|
218
|
+
.describe("指定するスタイル名(例: 'normal', 'ura', 'sleepy'など。省略時はキャラクターのデフォルト設定に従う)"),
|
|
219
|
+
},
|
|
220
|
+
}, async (args) => {
|
|
221
|
+
const { operator, style } = args || {};
|
|
222
|
+
logger.info('オペレータアサイン開始', { operator, style });
|
|
223
|
+
validateOperatorInput(operator);
|
|
224
|
+
try {
|
|
225
|
+
const assignResult = await assignOperator(operatorManager, operator, style);
|
|
226
|
+
logger.info('オペレータアサイン成功', {
|
|
227
|
+
characterId: assignResult.characterId,
|
|
228
|
+
characterName: assignResult.characterName,
|
|
229
|
+
});
|
|
230
|
+
// 背景画像を切り替え
|
|
231
|
+
if (terminalBackground) {
|
|
232
|
+
logger.error('🔧 TerminalBackground instance exists');
|
|
233
|
+
const isEnabled = await terminalBackground.isEnabled();
|
|
234
|
+
logger.error('📊 Terminal background enabled check:', { isEnabled });
|
|
235
|
+
if (isEnabled) {
|
|
236
|
+
logger.error('🔄 Switching background for character:', assignResult.characterId);
|
|
237
|
+
await terminalBackground.switchCharacter(assignResult.characterId);
|
|
238
|
+
logger.error('✅ 背景画像切り替え完了', { characterId: assignResult.characterId });
|
|
239
|
+
}
|
|
240
|
+
else {
|
|
241
|
+
logger.error('⚠️ Terminal background is not enabled');
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
else {
|
|
245
|
+
logger.error('❌ TerminalBackground instance is null');
|
|
246
|
+
}
|
|
247
|
+
const character = await operatorManager.getCharacterInfo(assignResult.characterId);
|
|
248
|
+
if (!character) {
|
|
249
|
+
throw new Error(`キャラクター情報が見つかりません: ${assignResult.characterId}`);
|
|
250
|
+
}
|
|
251
|
+
const availableStyles = extractStyleInfo(character);
|
|
252
|
+
const resultText = formatAssignmentResult(assignResult, availableStyles);
|
|
253
|
+
return {
|
|
254
|
+
content: [
|
|
255
|
+
{
|
|
256
|
+
type: 'text',
|
|
257
|
+
text: resultText,
|
|
258
|
+
},
|
|
259
|
+
],
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
catch (error) {
|
|
263
|
+
throw new Error(`オペレータ割り当てエラー: ${error.message}`);
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
server.registerTool('operator_release', {
|
|
267
|
+
description: '現在のオペレータを解放します',
|
|
268
|
+
inputSchema: {},
|
|
269
|
+
}, async () => {
|
|
270
|
+
try {
|
|
271
|
+
const result = await operatorManager.releaseOperator();
|
|
272
|
+
let releaseMessage;
|
|
273
|
+
if (result.wasAssigned) {
|
|
274
|
+
releaseMessage = `オペレータを解放しました: ${result.characterName}`;
|
|
275
|
+
logger.info(`オペレータ解放: ${result.characterId}`);
|
|
276
|
+
}
|
|
277
|
+
else {
|
|
278
|
+
releaseMessage = 'オペレータは割り当てられていません';
|
|
279
|
+
logger.info('オペレータ未割り当て状態');
|
|
280
|
+
}
|
|
281
|
+
// 背景画像をクリア(オペレータの有無に関わらず実行)
|
|
282
|
+
if (terminalBackground) {
|
|
283
|
+
if (await terminalBackground.isEnabled()) {
|
|
284
|
+
await terminalBackground.clearBackground();
|
|
285
|
+
logger.info('背景画像クリア完了');
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
return {
|
|
289
|
+
content: [
|
|
290
|
+
{
|
|
291
|
+
type: 'text',
|
|
292
|
+
text: releaseMessage,
|
|
293
|
+
},
|
|
294
|
+
],
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
catch (error) {
|
|
298
|
+
throw new Error(`オペレータ解放エラー: ${error.message}`);
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
server.registerTool('operator_status', {
|
|
302
|
+
description: '現在のオペレータ状況を確認します',
|
|
303
|
+
inputSchema: {},
|
|
304
|
+
}, async () => {
|
|
305
|
+
try {
|
|
306
|
+
const status = await operatorManager.showCurrentOperator();
|
|
307
|
+
return {
|
|
308
|
+
content: [
|
|
309
|
+
{
|
|
310
|
+
type: 'text',
|
|
311
|
+
text: status.message,
|
|
312
|
+
},
|
|
313
|
+
],
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
catch (error) {
|
|
317
|
+
throw new Error(`オペレータ状況確認エラー: ${error.message}`);
|
|
318
|
+
}
|
|
319
|
+
});
|
|
320
|
+
server.registerTool('operator_available', {
|
|
321
|
+
description: '利用可能なオペレータ一覧を表示します',
|
|
322
|
+
inputSchema: {},
|
|
323
|
+
}, async () => {
|
|
324
|
+
try {
|
|
325
|
+
const result = await operatorManager.getAvailableOperators();
|
|
326
|
+
let text = result.available.length > 0
|
|
327
|
+
? `利用可能なオペレータ: ${result.available.join(', ')}`
|
|
328
|
+
: '利用可能なオペレータがありません';
|
|
329
|
+
if (result.busy.length > 0) {
|
|
330
|
+
text += `\n仕事中のオペレータ: ${result.busy.join(', ')}`;
|
|
331
|
+
}
|
|
332
|
+
return {
|
|
333
|
+
content: [
|
|
334
|
+
{
|
|
335
|
+
type: 'text',
|
|
336
|
+
text: text,
|
|
337
|
+
},
|
|
338
|
+
],
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
catch (error) {
|
|
342
|
+
throw new Error(`利用可能オペレータ確認エラー: ${error.message}`);
|
|
343
|
+
}
|
|
344
|
+
});
|
|
345
|
+
// say音声出力ツール(src/say/index.js使用)
|
|
346
|
+
server.registerTool('say', {
|
|
347
|
+
description: 'COEIROINKを使って日本語音声を非同期で出力します(低レイテンシストリーミング対応、即座にレスポンス)',
|
|
348
|
+
inputSchema: {
|
|
349
|
+
message: z.string().describe('発話させるメッセージ(日本語)'),
|
|
350
|
+
voice: z.string().optional().describe('音声ID(省略時はオペレータ設定を使用)'),
|
|
351
|
+
rate: z.number().optional().describe('話速(WPM、デフォルト200)'),
|
|
352
|
+
style: z
|
|
353
|
+
.string()
|
|
354
|
+
.optional()
|
|
355
|
+
.describe("スタイル名(日本語名をそのまま指定。例: ディアちゃんの場合 'のーまる', 'ひそひそ', 'セクシー')"),
|
|
356
|
+
},
|
|
357
|
+
}, async (args) => {
|
|
358
|
+
const { message, voice, rate, style } = args;
|
|
359
|
+
try {
|
|
360
|
+
logger.debug('=== SAY TOOL DEBUG START ===');
|
|
361
|
+
logger.debug(`Input parameters:`);
|
|
362
|
+
logger.debug(` message: "${message}"`);
|
|
363
|
+
logger.debug(` voice: ${voice || 'null (will use operator voice)'}`);
|
|
364
|
+
logger.debug(` rate: ${rate || 'undefined (will use config default)'}`);
|
|
365
|
+
logger.debug(` style: ${style || 'undefined (will use operator default)'}`);
|
|
366
|
+
// Issue #58: オペレータ未アサイン時の再アサイン促進メッセージ
|
|
367
|
+
const currentOperator = await operatorManager.showCurrentOperator();
|
|
368
|
+
if (!currentOperator.characterId) {
|
|
369
|
+
// オペレータ未割り当て時に背景画像をクリア
|
|
370
|
+
if (terminalBackground) {
|
|
371
|
+
if (await terminalBackground.isEnabled()) {
|
|
372
|
+
await terminalBackground.clearBackground();
|
|
373
|
+
logger.info('オペレータ未割り当てのため背景画像をクリア');
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
// 利用可能なオペレータを取得
|
|
377
|
+
let availableOperators = [];
|
|
378
|
+
try {
|
|
379
|
+
const result = await operatorManager.getAvailableOperators();
|
|
380
|
+
availableOperators = result.available;
|
|
381
|
+
}
|
|
382
|
+
catch (error) {
|
|
383
|
+
logger.warn(`Failed to get available operators: ${error.message}`);
|
|
384
|
+
}
|
|
385
|
+
let guidanceMessage = '⚠️ オペレータが割り当てられていません。\n\n';
|
|
386
|
+
guidanceMessage += '🔧 次の手順で進めてください:\n';
|
|
387
|
+
guidanceMessage += '1. operator_assign を実行してオペレータを選択\n';
|
|
388
|
+
guidanceMessage += '2. 再度 say コマンドで音声を生成\n\n';
|
|
389
|
+
if (availableOperators.length > 0) {
|
|
390
|
+
guidanceMessage += `🎭 利用可能なオペレータ: ${availableOperators.join(', ')}\n\n`;
|
|
391
|
+
guidanceMessage +=
|
|
392
|
+
"💡 例:operator_assign ツールで 'tsukuyomi' を選択する場合は、operator パラメータに 'tsukuyomi' を指定してください。";
|
|
393
|
+
}
|
|
394
|
+
else {
|
|
395
|
+
guidanceMessage +=
|
|
396
|
+
'❌ 現在利用可能なオペレータがありません。しばらく待ってから再試行してください。';
|
|
397
|
+
}
|
|
398
|
+
return {
|
|
399
|
+
content: [
|
|
400
|
+
{
|
|
401
|
+
type: 'text',
|
|
402
|
+
text: guidanceMessage,
|
|
403
|
+
},
|
|
404
|
+
],
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
// Issue #58: 動的タイムアウト延長 - sayコマンド実行時にオペレータ予約を延長
|
|
408
|
+
// ベストエフォート非同期処理(エラーは無視、音声生成をブロックしない)
|
|
409
|
+
operatorManager
|
|
410
|
+
.refreshOperatorReservation()
|
|
411
|
+
.then(refreshSuccess => {
|
|
412
|
+
if (refreshSuccess) {
|
|
413
|
+
logger.debug(`Operator reservation refreshed for: ${currentOperator.characterId}`);
|
|
414
|
+
}
|
|
415
|
+
else {
|
|
416
|
+
logger.debug(`Could not refresh operator reservation for: ${currentOperator.characterId} (not critical)`);
|
|
417
|
+
}
|
|
418
|
+
})
|
|
419
|
+
.catch(error => {
|
|
420
|
+
logger.debug(`Operator reservation refresh failed: ${error.message} (not critical)`);
|
|
421
|
+
});
|
|
422
|
+
// スタイル検証(事前チェック)
|
|
423
|
+
if (style && currentOperator.characterId) {
|
|
424
|
+
try {
|
|
425
|
+
const character = await operatorManager.getCharacterInfo(currentOperator.characterId);
|
|
426
|
+
if (!character) {
|
|
427
|
+
throw new Error(`キャラクター情報が取得できません`);
|
|
428
|
+
}
|
|
429
|
+
// 利用可能なスタイルを取得
|
|
430
|
+
const availableStyles = character.speaker?.styles || [];
|
|
431
|
+
// 指定されたスタイルが存在するか確認
|
|
432
|
+
const styleExists = availableStyles.some(s => s.styleName === style);
|
|
433
|
+
if (!styleExists) {
|
|
434
|
+
const styleNames = availableStyles.map(s => s.styleName);
|
|
435
|
+
throw new Error(`指定されたスタイル '${style}' が見つかりません。利用可能なスタイル: ${styleNames.join(', ')}`);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
catch (error) {
|
|
439
|
+
logger.error(`スタイル検証エラー: ${error.message}`);
|
|
440
|
+
throw error;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
// 設定情報をログ出力
|
|
444
|
+
// NOTE: ConfigManagerはすでにsayCoeiroink内部で管理されているため、
|
|
445
|
+
// ここでは設定のログ出力をスキップ
|
|
446
|
+
logger.debug('Audio config is managed internally by SayCoeiroink');
|
|
447
|
+
logger.debug('==============================');
|
|
448
|
+
// MCP設計: 音声合成タスクをキューに投稿のみ(再生完了を待たない)
|
|
449
|
+
// - synthesize() はキューに追加して即座にレスポンス
|
|
450
|
+
// - 実際の音声合成・再生は背景のSpeechQueueで非同期処理
|
|
451
|
+
// - CLIとは異なり、MCPではウォームアップ・完了待機は実行しない
|
|
452
|
+
const result = sayCoeiroink.synthesize(message, {
|
|
453
|
+
voice: voice || null,
|
|
454
|
+
rate: rate || undefined,
|
|
455
|
+
style: style || undefined,
|
|
456
|
+
allowFallback: false, // MCPツールではオペレータが必須
|
|
457
|
+
});
|
|
458
|
+
// 結果をログ出力
|
|
459
|
+
logger.debug(`Result: ${JSON.stringify(result)}`);
|
|
460
|
+
const modeInfo = `発声キューに追加 - オペレータ: ${currentOperator.characterId}, タスクID: ${result.taskId}`;
|
|
461
|
+
logger.info(modeInfo);
|
|
462
|
+
logger.debug('=== SAY TOOL DEBUG END ===');
|
|
463
|
+
// 即座にレスポンスを返す(音声合成の完了を待たない)
|
|
464
|
+
const responseText = `音声合成を開始しました - オペレータ: ${currentOperator.characterId}`;
|
|
465
|
+
return {
|
|
466
|
+
content: [
|
|
467
|
+
{
|
|
468
|
+
type: 'text',
|
|
469
|
+
text: responseText,
|
|
470
|
+
},
|
|
471
|
+
],
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
catch (error) {
|
|
475
|
+
logger.debug(`SAY TOOL ERROR: ${error.message}`);
|
|
476
|
+
logger.debug(`Stack trace: ${error.stack}`);
|
|
477
|
+
throw new Error(`音声出力エラー: ${error.message}`);
|
|
478
|
+
}
|
|
479
|
+
});
|
|
480
|
+
// ログ取得ツール
|
|
481
|
+
server.registerTool('debug_logs', {
|
|
482
|
+
description: 'デバッグ用ログの取得と表示。ログレベル・時刻・検索条件による絞り込み、統計情報の表示が可能',
|
|
483
|
+
inputSchema: {
|
|
484
|
+
action: z
|
|
485
|
+
.enum(['get', 'stats', 'clear'])
|
|
486
|
+
.describe('実行するアクション: get=ログ取得, stats=統計表示, clear=ログクリア'),
|
|
487
|
+
level: z
|
|
488
|
+
.array(z.enum(['error', 'warn', 'info', 'verbose', 'debug']))
|
|
489
|
+
.optional()
|
|
490
|
+
.describe('取得するログレベル(複数選択可)'),
|
|
491
|
+
since: z.string().optional().describe('この時刻以降のログを取得(ISO 8601形式)'),
|
|
492
|
+
limit: z
|
|
493
|
+
.number()
|
|
494
|
+
.min(1)
|
|
495
|
+
.max(1000)
|
|
496
|
+
.optional()
|
|
497
|
+
.describe('取得する最大ログエントリ数(1-1000)'),
|
|
498
|
+
search: z.string().optional().describe('ログメッセージ内の検索キーワード'),
|
|
499
|
+
format: z
|
|
500
|
+
.enum(['formatted', 'raw'])
|
|
501
|
+
.optional()
|
|
502
|
+
.describe('出力形式: formatted=整形済み, raw=生データ'),
|
|
503
|
+
},
|
|
504
|
+
}, async (args) => {
|
|
505
|
+
const { action = 'get', level, since, limit, search, format = 'formatted' } = args || {};
|
|
506
|
+
try {
|
|
507
|
+
switch (action) {
|
|
508
|
+
case 'get': {
|
|
509
|
+
const options = {};
|
|
510
|
+
if (level && level.length > 0) {
|
|
511
|
+
options.level = level;
|
|
512
|
+
}
|
|
513
|
+
if (since) {
|
|
514
|
+
try {
|
|
515
|
+
options.since = new Date(since);
|
|
516
|
+
}
|
|
517
|
+
catch {
|
|
518
|
+
throw new Error(`無効な日時形式です: ${since}`);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
if (limit) {
|
|
522
|
+
options.limit = limit;
|
|
523
|
+
}
|
|
524
|
+
if (search) {
|
|
525
|
+
options.search = search;
|
|
526
|
+
}
|
|
527
|
+
const entries = logger.getLogEntries(options);
|
|
528
|
+
if (entries.length === 0) {
|
|
529
|
+
return {
|
|
530
|
+
content: [
|
|
531
|
+
{
|
|
532
|
+
type: 'text',
|
|
533
|
+
text: '条件に一致するログエントリが見つかりませんでした。',
|
|
534
|
+
},
|
|
535
|
+
],
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
let resultText;
|
|
539
|
+
if (format === 'raw') {
|
|
540
|
+
resultText = `ログエントリ (${entries.length}件):\n\n${JSON.stringify(entries, null, 2)}`;
|
|
541
|
+
}
|
|
542
|
+
else {
|
|
543
|
+
resultText = `ログエントリ (${entries.length}件):\n\n`;
|
|
544
|
+
entries.forEach((entry, index) => {
|
|
545
|
+
resultText += `${index + 1}. [${entry.level.toUpperCase()}] ${entry.timestamp}\n`;
|
|
546
|
+
resultText += ` ${entry.message}\n`;
|
|
547
|
+
if (entry.args && entry.args.length > 0) {
|
|
548
|
+
resultText += ` 引数: ${entry.args
|
|
549
|
+
.map(arg => (typeof arg === 'object' ? JSON.stringify(arg) : String(arg)))
|
|
550
|
+
.join(', ')}\n`;
|
|
551
|
+
}
|
|
552
|
+
resultText += '\n';
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
return {
|
|
556
|
+
content: [
|
|
557
|
+
{
|
|
558
|
+
type: 'text',
|
|
559
|
+
text: resultText,
|
|
560
|
+
},
|
|
561
|
+
],
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
case 'stats': {
|
|
565
|
+
const stats = logger.getLogStats();
|
|
566
|
+
const statsText = `📊 ログ統計情報\n\n` +
|
|
567
|
+
`総エントリ数: ${stats.totalEntries}\n\n` +
|
|
568
|
+
`レベル別エントリ数:\n` +
|
|
569
|
+
` ERROR: ${stats.entriesByLevel.error}\n` +
|
|
570
|
+
` WARN: ${stats.entriesByLevel.warn}\n` +
|
|
571
|
+
` INFO: ${stats.entriesByLevel.info}\n` +
|
|
572
|
+
` VERB: ${stats.entriesByLevel.verbose}\n` +
|
|
573
|
+
` DEBUG: ${stats.entriesByLevel.debug}\n\n` +
|
|
574
|
+
`時刻範囲:\n` +
|
|
575
|
+
` 最古: ${stats.oldestEntry || 'なし'}\n` +
|
|
576
|
+
` 最新: ${stats.newestEntry || 'なし'}\n\n` +
|
|
577
|
+
`蓄積モード: ${logger.isAccumulating() ? 'ON' : 'OFF'}`;
|
|
578
|
+
return {
|
|
579
|
+
content: [
|
|
580
|
+
{
|
|
581
|
+
type: 'text',
|
|
582
|
+
text: statsText,
|
|
583
|
+
},
|
|
584
|
+
],
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
case 'clear': {
|
|
588
|
+
const beforeCount = logger.getLogStats().totalEntries;
|
|
589
|
+
logger.clearLogEntries();
|
|
590
|
+
return {
|
|
591
|
+
content: [
|
|
592
|
+
{
|
|
593
|
+
type: 'text',
|
|
594
|
+
text: `ログエントリをクリアしました(${beforeCount}件削除)`,
|
|
595
|
+
},
|
|
596
|
+
],
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
default:
|
|
600
|
+
throw new Error(`無効なアクション: ${action}`);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
catch (error) {
|
|
604
|
+
throw new Error(`ログ取得エラー: ${error.message}`);
|
|
605
|
+
}
|
|
606
|
+
});
|
|
607
|
+
// スタイル情報表示ツール
|
|
608
|
+
server.registerTool('operator_styles', {
|
|
609
|
+
description: '現在のオペレータまたは指定したキャラクターの利用可能なスタイル一覧を表示します。キャラクターの基本情報、全スタイルの詳細(性格・話し方)、スタイル選択方法を確認できます。スタイル切り替えにはsayツールのstyleパラメータで日本語名を使用してください。',
|
|
610
|
+
inputSchema: {
|
|
611
|
+
character: z
|
|
612
|
+
.string()
|
|
613
|
+
.optional()
|
|
614
|
+
.describe('キャラクターID(省略時は現在のオペレータのスタイル情報を表示)'),
|
|
615
|
+
},
|
|
616
|
+
}, async (args) => {
|
|
617
|
+
const { character } = args || {};
|
|
618
|
+
try {
|
|
619
|
+
let targetCharacter;
|
|
620
|
+
let targetCharacterId;
|
|
621
|
+
if (character) {
|
|
622
|
+
// 指定されたキャラクターの情報を取得
|
|
623
|
+
try {
|
|
624
|
+
targetCharacter = await operatorManager.getCharacterInfo(character);
|
|
625
|
+
if (!targetCharacter) {
|
|
626
|
+
throw new Error(`キャラクター '${character}' が見つかりません`);
|
|
627
|
+
}
|
|
628
|
+
targetCharacterId = character;
|
|
629
|
+
}
|
|
630
|
+
catch (error) {
|
|
631
|
+
throw new Error(`キャラクター '${character}' が見つかりません`);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
else {
|
|
635
|
+
// 現在のオペレータの情報を取得
|
|
636
|
+
const currentOperator = await operatorManager.showCurrentOperator();
|
|
637
|
+
if (!currentOperator.characterId) {
|
|
638
|
+
throw new Error('現在オペレータが割り当てられていません。まず operator_assign を実行してください。');
|
|
639
|
+
}
|
|
640
|
+
targetCharacter = await operatorManager.getCharacterInfo(currentOperator.characterId);
|
|
641
|
+
targetCharacterId = currentOperator.characterId;
|
|
642
|
+
if (!targetCharacter) {
|
|
643
|
+
throw new Error(`現在のオペレータ '${currentOperator.characterId}' のキャラクター情報が見つかりません`);
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
// スタイル情報を取得
|
|
647
|
+
const availableStyles = extractStyleInfo(targetCharacter);
|
|
648
|
+
// 結果を整形
|
|
649
|
+
const resultText = formatStylesResult(targetCharacter, availableStyles);
|
|
650
|
+
return {
|
|
651
|
+
content: [
|
|
652
|
+
{
|
|
653
|
+
type: 'text',
|
|
654
|
+
text: resultText,
|
|
655
|
+
},
|
|
656
|
+
],
|
|
657
|
+
};
|
|
658
|
+
}
|
|
659
|
+
catch (error) {
|
|
660
|
+
throw new Error(`スタイル情報取得エラー: ${error.message}`);
|
|
661
|
+
}
|
|
662
|
+
});
|
|
663
|
+
// 並行生成制御ツール
|
|
664
|
+
server.registerTool('parallel_generation_control', {
|
|
665
|
+
description: 'チャンク並行生成機能の制御と設定管理を行います。生成の並行数、待機時間、先読み数、初回ポーズ機能などを調整できます。',
|
|
666
|
+
inputSchema: {
|
|
667
|
+
action: z
|
|
668
|
+
.enum(['enable', 'disable', 'status', 'update_options'])
|
|
669
|
+
.describe('実行するアクション'),
|
|
670
|
+
options: z
|
|
671
|
+
.object({
|
|
672
|
+
maxConcurrency: z.number().min(1).max(5).optional().describe('最大並行生成数(1-5)'),
|
|
673
|
+
delayBetweenRequests: z
|
|
674
|
+
.number()
|
|
675
|
+
.min(0)
|
|
676
|
+
.max(1000)
|
|
677
|
+
.optional()
|
|
678
|
+
.describe('リクエスト間隔(ms、0-1000)'),
|
|
679
|
+
bufferAheadCount: z.number().min(0).max(3).optional().describe('先読みチャンク数(0-3)'),
|
|
680
|
+
pauseUntilFirstComplete: z
|
|
681
|
+
.boolean()
|
|
682
|
+
.optional()
|
|
683
|
+
.describe('初回チャンク完了まで並行生成をポーズ(レイテンシ改善)'),
|
|
684
|
+
})
|
|
685
|
+
.optional()
|
|
686
|
+
.describe('更新するオプション(action=update_optionsの場合)'),
|
|
687
|
+
},
|
|
688
|
+
}, async (args) => {
|
|
689
|
+
const { action, options } = args || {};
|
|
690
|
+
try {
|
|
691
|
+
switch (action) {
|
|
692
|
+
case 'enable':
|
|
693
|
+
sayCoeiroink.setParallelGenerationEnabled(true);
|
|
694
|
+
return {
|
|
695
|
+
content: [
|
|
696
|
+
{
|
|
697
|
+
type: 'text',
|
|
698
|
+
text: '✅ 並行チャンク生成を有効化しました。\n\n⚡ 効果:\n- 複数チャンクの同時生成により高速化\n- レスポンシブな音声再生開始\n- 体感的なレイテンシ削減',
|
|
699
|
+
},
|
|
700
|
+
],
|
|
701
|
+
};
|
|
702
|
+
case 'disable':
|
|
703
|
+
sayCoeiroink.setParallelGenerationEnabled(false);
|
|
704
|
+
return {
|
|
705
|
+
content: [
|
|
706
|
+
{
|
|
707
|
+
type: 'text',
|
|
708
|
+
text: '⏸️ 並行チャンク生成を無効化しました。\n\n🔄 従来の逐次生成モードに戻りました。\n- 安定性重視の動作\n- メモリ使用量削減',
|
|
709
|
+
},
|
|
710
|
+
],
|
|
711
|
+
};
|
|
712
|
+
case 'status': {
|
|
713
|
+
const currentOptions = sayCoeiroink.getStreamControllerOptions();
|
|
714
|
+
const stats = sayCoeiroink.getGenerationStats();
|
|
715
|
+
return {
|
|
716
|
+
content: [
|
|
717
|
+
{
|
|
718
|
+
type: 'text',
|
|
719
|
+
text: `📊 並行生成ステータス\n\n` +
|
|
720
|
+
`🎛️ 設定:\n` +
|
|
721
|
+
` - 状態: ${currentOptions.maxConcurrency > 1 ? '✅ 並行生成' : '❌ 逐次生成'}\n` +
|
|
722
|
+
` - 最大並行数: ${currentOptions.maxConcurrency} ${currentOptions.maxConcurrency === 1 ? '(逐次モード)' : '(並行モード)'}\n` +
|
|
723
|
+
` - リクエスト間隔: ${currentOptions.delayBetweenRequests}ms\n` +
|
|
724
|
+
` - 先読み数: ${currentOptions.bufferAheadCount}\n` +
|
|
725
|
+
` - 初回ポーズ: ${currentOptions.pauseUntilFirstComplete ? '✅ 有効' : '❌ 無効'}\n\n` +
|
|
726
|
+
`📈 現在の統計:\n` +
|
|
727
|
+
` - アクティブタスク: ${stats.activeTasks}\n` +
|
|
728
|
+
` - 完了済み結果: ${stats.completedResults}\n` +
|
|
729
|
+
` - メモリ使用量: ${(stats.totalMemoryUsage / 1024).toFixed(1)}KB`,
|
|
730
|
+
},
|
|
731
|
+
],
|
|
732
|
+
};
|
|
733
|
+
}
|
|
734
|
+
case 'update_options':
|
|
735
|
+
if (options) {
|
|
736
|
+
sayCoeiroink.updateStreamControllerOptions(options);
|
|
737
|
+
const updatedOptions = sayCoeiroink.getStreamControllerOptions();
|
|
738
|
+
return {
|
|
739
|
+
content: [
|
|
740
|
+
{
|
|
741
|
+
type: 'text',
|
|
742
|
+
text: `⚙️ オプション更新完了\n\n` +
|
|
743
|
+
`🔧 新しい設定:\n` +
|
|
744
|
+
` - 最大並行数: ${updatedOptions.maxConcurrency} ${updatedOptions.maxConcurrency === 1 ? '(逐次モード)' : '(並行モード)'}\n` +
|
|
745
|
+
` - リクエスト間隔: ${updatedOptions.delayBetweenRequests}ms\n` +
|
|
746
|
+
` - 先読み数: ${updatedOptions.bufferAheadCount}\n` +
|
|
747
|
+
` - 初回ポーズ: ${updatedOptions.pauseUntilFirstComplete ? '✅ 有効' : '❌ 無効'}\n\n` +
|
|
748
|
+
`💡 次回の音声合成から適用されます。`,
|
|
749
|
+
},
|
|
750
|
+
],
|
|
751
|
+
};
|
|
752
|
+
}
|
|
753
|
+
else {
|
|
754
|
+
throw new Error('update_optionsアクションにはoptionsパラメータが必要です');
|
|
755
|
+
}
|
|
756
|
+
default:
|
|
757
|
+
throw new Error(`無効なアクション: ${action}`);
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
catch (error) {
|
|
761
|
+
throw new Error(`並行生成制御エラー: ${error.message}`);
|
|
762
|
+
}
|
|
763
|
+
});
|
|
764
|
+
// 辞書登録ツール
|
|
765
|
+
server.registerTool('dictionary_register', {
|
|
766
|
+
description: 'COEIROINKのユーザー辞書に単語を登録します。専門用語や固有名詞の読み方を正確に制御できます。',
|
|
767
|
+
inputSchema: {
|
|
768
|
+
word: z.string().describe('登録する単語(半角英数字も可、自動で全角変換されます)'),
|
|
769
|
+
yomi: z.string().describe('読み方(全角カタカナ)'),
|
|
770
|
+
accent: z.number().describe('アクセント位置(0:平板型、1以上:該当モーラが高い)'),
|
|
771
|
+
numMoras: z.number().describe('モーラ数(カタカナの音節数)'),
|
|
772
|
+
},
|
|
773
|
+
}, async (args) => {
|
|
774
|
+
const { word, yomi, accent, numMoras } = args;
|
|
775
|
+
try {
|
|
776
|
+
// 接続確認
|
|
777
|
+
const isConnected = await dictionaryService.checkConnection();
|
|
778
|
+
if (!isConnected) {
|
|
779
|
+
return {
|
|
780
|
+
content: [
|
|
781
|
+
{
|
|
782
|
+
type: 'text',
|
|
783
|
+
text: '❌ COEIROINKサーバーに接続できません。\n' +
|
|
784
|
+
'サーバーが起動していることを確認してください。',
|
|
785
|
+
},
|
|
786
|
+
],
|
|
787
|
+
};
|
|
788
|
+
}
|
|
789
|
+
// 単語を登録(DictionaryServiceが永続化まで処理)
|
|
790
|
+
const success = await dictionaryService.addWord({ word, yomi, accent, numMoras });
|
|
791
|
+
if (success) {
|
|
792
|
+
return {
|
|
793
|
+
content: [
|
|
794
|
+
{
|
|
795
|
+
type: 'text',
|
|
796
|
+
text: `✅ 単語を辞書に登録しました\n\n` +
|
|
797
|
+
`単語: ${word}\n` +
|
|
798
|
+
`読み方: ${yomi}\n` +
|
|
799
|
+
`アクセント: ${accent}\n` +
|
|
800
|
+
`モーラ数: ${numMoras}\n\n` +
|
|
801
|
+
`💾 辞書データは永続化され、次回起動時に自動登録されます。`,
|
|
802
|
+
},
|
|
803
|
+
],
|
|
804
|
+
};
|
|
805
|
+
}
|
|
806
|
+
else {
|
|
807
|
+
return {
|
|
808
|
+
content: [
|
|
809
|
+
{
|
|
810
|
+
type: 'text',
|
|
811
|
+
text: `❌ 辞書登録に失敗しました`,
|
|
812
|
+
},
|
|
813
|
+
],
|
|
814
|
+
};
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
catch (error) {
|
|
818
|
+
logger.error(`Dictionary registration error:`, error);
|
|
819
|
+
throw new Error(`辞書登録エラー: ${error.message}`);
|
|
820
|
+
}
|
|
821
|
+
});
|
|
822
|
+
// サーバーの起動
|
|
823
|
+
async function main() {
|
|
824
|
+
const transport = new StdioServerTransport();
|
|
825
|
+
logger.info('Say COEIROINK MCP Server starting...');
|
|
826
|
+
await server.connect(transport);
|
|
827
|
+
logger.info('Say COEIROINK MCP Server started');
|
|
828
|
+
}
|
|
829
|
+
main().catch(error => {
|
|
830
|
+
logger.error('Server error:', error);
|
|
831
|
+
process.exit(1);
|
|
832
|
+
});
|
|
833
|
+
//# sourceMappingURL=server.js.map
|
package/package.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@coeiro-operator/mcp",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "MCP server for COEIRO Operator",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/server.js",
|
|
7
|
+
"types": "dist/server.d.ts",
|
|
8
|
+
"os": [
|
|
9
|
+
"darwin",
|
|
10
|
+
"linux",
|
|
11
|
+
"win32"
|
|
12
|
+
],
|
|
13
|
+
"cpu": [
|
|
14
|
+
"x64",
|
|
15
|
+
"arm64",
|
|
16
|
+
"ia32"
|
|
17
|
+
],
|
|
18
|
+
"bin": {
|
|
19
|
+
"coeiro-operator": "./dist/server.js"
|
|
20
|
+
},
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "tsc --build",
|
|
23
|
+
"start": "node dist/server.js",
|
|
24
|
+
"test": "vitest",
|
|
25
|
+
"test:watch": "vitest --watch",
|
|
26
|
+
"type-check": "tsc --noEmit",
|
|
27
|
+
"lint": "eslint src --ext .ts,.js",
|
|
28
|
+
"format": "prettier --write \"src/**/*.{ts,js,json,md}\""
|
|
29
|
+
},
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"@coeiro-operator/common": "^1.0.0",
|
|
32
|
+
"@coeiro-operator/core": "^1.0.0",
|
|
33
|
+
"@coeiro-operator/audio": "^1.0.0",
|
|
34
|
+
"@modelcontextprotocol/sdk": "^1.17.0"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@types/node": "^22.18.3",
|
|
38
|
+
"typescript": "^5.7.3",
|
|
39
|
+
"vitest": "^3.2.4"
|
|
40
|
+
},
|
|
41
|
+
"keywords": [
|
|
42
|
+
"mcp",
|
|
43
|
+
"server",
|
|
44
|
+
"coeiroink"
|
|
45
|
+
],
|
|
46
|
+
"license": "MIT",
|
|
47
|
+
"repository": {
|
|
48
|
+
"type": "git",
|
|
49
|
+
"url": "git+https://github.com/otolab/coeiro-operator.git",
|
|
50
|
+
"directory": "packages/mcp"
|
|
51
|
+
},
|
|
52
|
+
"homepage": "https://github.com/otolab/coeiro-operator#readme",
|
|
53
|
+
"bugs": {
|
|
54
|
+
"url": "https://github.com/otolab/coeiro-operator/issues"
|
|
55
|
+
},
|
|
56
|
+
"publishConfig": {
|
|
57
|
+
"access": "public"
|
|
58
|
+
}
|
|
59
|
+
}
|