poker-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.
Files changed (50) hide show
  1. package/README.md +241 -0
  2. package/config/mcp-manifest.json +1713 -0
  3. package/config/package-lock.json +1335 -0
  4. package/config/poker_mcp.code-workspace +8 -0
  5. package/package.json +51 -0
  6. package/src/config/ConfigManager.js +120 -0
  7. package/src/config/default.json +62 -0
  8. package/src/mcp/handlers/bodyHandlers.js +63 -0
  9. package/src/mcp/handlers/buildupFactorHandlers.js +43 -0
  10. package/src/mcp/handlers/calculationHandlers.js +265 -0
  11. package/src/mcp/handlers/detectorHandlers.js +201 -0
  12. package/src/mcp/handlers/index.js +43 -0
  13. package/src/mcp/handlers/sourceHandlers.js +25 -0
  14. package/src/mcp/handlers/transformHandlers.js +26 -0
  15. package/src/mcp/handlers/unitHandlers.js +305 -0
  16. package/src/mcp/handlers/zoneHandlers.js +83 -0
  17. package/src/mcp/middleware/errorHandler.js +49 -0
  18. package/src/mcp/middleware/requestValidator.js +129 -0
  19. package/src/mcp/server.js +76 -0
  20. package/src/mcp/tools/bodyTools.js +185 -0
  21. package/src/mcp/tools/buildupFactorTools.js +82 -0
  22. package/src/mcp/tools/calculationTools.js +58 -0
  23. package/src/mcp/tools/commonTools.js +22 -0
  24. package/src/mcp/tools/detectorTools.js +131 -0
  25. package/src/mcp/tools/index.js +34 -0
  26. package/src/mcp/tools/sourceTools.js +726 -0
  27. package/src/mcp/tools/sourceTools_fixed_part2.js +41 -0
  28. package/src/mcp/tools/transformTools.js +73 -0
  29. package/src/mcp/tools/unitTools.js +137 -0
  30. package/src/mcp/tools/zoneTools.js +65 -0
  31. package/src/mcp_server_stdio_v4.js +45 -0
  32. package/src/routes/restApi.js +184 -0
  33. package/src/services/CalculationService.js +525 -0
  34. package/src/services/DataManager.js +738 -0
  35. package/src/services/TaskManager.js +1622 -0
  36. package/src/utils/MaterialAlternatives.js +189 -0
  37. package/src/utils/errorHandler.js +68 -0
  38. package/src/utils/errors.js +37 -0
  39. package/src/utils/logger.js +24 -0
  40. package/src/utils/logger_no_color.js +28 -0
  41. package/src/utils/mcpErrors.js +225 -0
  42. package/src/validators/CMBValidator.js +191 -0
  43. package/src/validators/DetectorValidator.js +512 -0
  44. package/src/validators/ManifestValidator.js +249 -0
  45. package/src/validators/NuclideValidator.js +171 -0
  46. package/src/validators/PhysicsValidator.js +401 -0
  47. package/src/validators/PhysicsValidatorSimple.js +87 -0
  48. package/src/validators/SourceValidator.js +515 -0
  49. package/src/validators/TransformValidator.js +472 -0
  50. package/src/validators/UnitValidator.js +562 -0
@@ -0,0 +1,525 @@
1
+ // services/CalculationService.js
2
+ import { spawn } from 'child_process';
3
+ import { promises as fs } from 'fs';
4
+ import path from 'path';
5
+ import { logger } from '../utils/logger.js';
6
+ import { CalculationError } from '../utils/errors.js';
7
+
8
+ /**
9
+ * 放射線遮蔽計算実行サービス
10
+ * poker_cuiコマンドとの連携を担当
11
+ */
12
+ export class CalculationService {
13
+ constructor() {
14
+ this.pokercuiCommand = 'poker_cui';
15
+ this.defaultTimeout = 300000; // 5分
16
+ }
17
+
18
+ /**
19
+ * poker_cuiコマンドの利用可能性をチェック
20
+ * @returns {Promise<boolean>} コマンドが利用可能かどうか
21
+ */
22
+ async checkPokerCuiAvailability() {
23
+ return new Promise((resolve) => {
24
+ const child = spawn(this.pokercuiCommand, ['--version'], {
25
+ stdio: 'pipe',
26
+ shell: true
27
+ });
28
+
29
+ let hasOutput = false;
30
+
31
+ child.stdout.on('data', (data) => {
32
+ hasOutput = true;
33
+ logger.debug('poker_cui version check output', { output: data.toString() });
34
+ });
35
+
36
+ child.stderr.on('data', (data) => {
37
+ hasOutput = true;
38
+ logger.debug('poker_cui version check stderr', { error: data.toString() });
39
+ });
40
+
41
+ child.on('close', (code) => {
42
+ // コードが0でなくても、出力があればコマンドが存在する
43
+ resolve(hasOutput || code === 0);
44
+ });
45
+
46
+ child.on('error', (error) => {
47
+ logger.debug('poker_cui availability check failed', { error: error.message });
48
+ resolve(false);
49
+ });
50
+
51
+ // 3秒でタイムアウト
52
+ setTimeout(() => {
53
+ child.kill();
54
+ resolve(false);
55
+ }, 3000);
56
+ });
57
+ }
58
+
59
+ /**
60
+ * YAMLファイルパスが絶対パス形式かどうかを検証
61
+ * @param {string} yamlFile YAMLファイルパス
62
+ * @returns {boolean} 絶対パス形式かどうか
63
+ */
64
+ validateAbsolutePath(yamlFile) {
65
+ // 新しい絶対パス対応パターン(マニフェストと同じ)
66
+ const pattern = /^([a-zA-Z]:[\\\/]|\/)/;
67
+ return pattern.test(yamlFile);
68
+ }
69
+
70
+ /**
71
+ * YAMLファイルの完全検証(絶対パス + 存在 + 読み取り可能性)
72
+ * @param {string} yamlFile YAMLファイルパス
73
+ * @returns {Promise<{valid: boolean, error?: string}>} 検証結果
74
+ */
75
+ async validateYamlFile(yamlFile) {
76
+ // 1. 絶対パス形式の検証
77
+ if (!this.validateAbsolutePath(yamlFile)) {
78
+ return {
79
+ valid: false,
80
+ error: 'YAMLファイルは絶対パスで指定してください。例: C:\\path\\to\\file.yaml または /path/to/file.yaml'
81
+ };
82
+ }
83
+
84
+ // 2. ファイル拡張子の検証
85
+ const extPattern = /\.(yaml|yml)$/i;
86
+ if (!extPattern.test(yamlFile)) {
87
+ return {
88
+ valid: false,
89
+ error: 'YAMLファイルは .yaml または .yml 拡張子を持つ必要があります'
90
+ };
91
+ }
92
+
93
+ // 3. ファイル存在確認
94
+ try {
95
+ const stats = await fs.stat(yamlFile);
96
+ if (!stats.isFile()) {
97
+ return {
98
+ valid: false,
99
+ error: '指定されたパスはファイルではありません'
100
+ };
101
+ }
102
+ } catch (error) {
103
+ return {
104
+ valid: false,
105
+ error: `指定されたファイルが存在しません: ${yamlFile}`
106
+ };
107
+ }
108
+
109
+ // 4. ファイルアクセス権限確認
110
+ try {
111
+ await fs.access(yamlFile, fs.constants.R_OK);
112
+ return { valid: true };
113
+ } catch (error) {
114
+ return {
115
+ valid: false,
116
+ error: `ファイルへの読み取り権限がありません: ${yamlFile}`
117
+ };
118
+ }
119
+ }
120
+
121
+ /**
122
+ * YAMLファイルの存在と読み取り可能性をチェック(後方互換用)
123
+ * @param {string} yamlFile YAMLファイルパス
124
+ * @returns {Promise<boolean>} ファイルが存在し読み取り可能かどうか
125
+ */
126
+ async checkYamlFileExistence(yamlFile) {
127
+ const validation = await this.validateYamlFile(yamlFile);
128
+ return validation.valid;
129
+ }
130
+
131
+ /**
132
+ * YAML構文の基本チェック
133
+ * @param {string} yamlFile YAMLファイルパス
134
+ * @returns {Promise<{valid: boolean, error?: string}>} 構文チェック結果
135
+ */
136
+ async validateYamlSyntax(yamlFile) {
137
+ try {
138
+ const content = await fs.readFile(yamlFile, 'utf8');
139
+
140
+ // 基本的な構文チェック(簡易版)
141
+ const lines = content.split('\n');
142
+ let indentLevel = 0;
143
+ let hasBody = false;
144
+ let hasZone = false;
145
+ let hasSource = false;
146
+
147
+ for (let i = 0; i < lines.length; i++) {
148
+ const line = lines[i].trim();
149
+ if (line === '' || line.startsWith('#')) continue;
150
+
151
+ // セクション存在チェック
152
+ if (line === 'body:') hasBody = true;
153
+ if (line === 'zone:') hasZone = true;
154
+ if (line === 'source:') hasSource = true;
155
+
156
+ // 基本的なYAML構文チェック
157
+ if (line.includes(':') && !line.startsWith('-')) {
158
+ const colonIndex = line.indexOf(':');
159
+ const key = line.substring(0, colonIndex).trim();
160
+ if (key.includes(' ') && !key.startsWith('"') && !key.startsWith("'")) {
161
+ return {
162
+ valid: false,
163
+ error: `Line ${i + 1}: Invalid key format '${key}' - keys with spaces must be quoted`
164
+ };
165
+ }
166
+ }
167
+ }
168
+
169
+ // 必須セクションチェック
170
+ if (!hasBody) {
171
+ return { valid: false, error: 'Missing required section: body' };
172
+ }
173
+ if (!hasZone) {
174
+ return { valid: false, error: 'Missing required section: zone' };
175
+ }
176
+ if (!hasSource) {
177
+ return { valid: false, error: 'Missing required section: source' };
178
+ }
179
+
180
+ return { valid: true };
181
+ } catch (error) {
182
+ return {
183
+ valid: false,
184
+ error: `YAML syntax error: ${error.message}`
185
+ };
186
+ }
187
+ }
188
+
189
+ /**
190
+ * サマリーオプションの検証と正規化
191
+ * @param {Object} summaryOptions サマリーオプション
192
+ * @returns {Object} 正規化されたサマリーオプション
193
+ */
194
+ validateAndNormalizeSummaryOptions(summaryOptions = {}) {
195
+ const normalized = {
196
+ show_parameters: summaryOptions.show_parameters || false,
197
+ show_source_data: summaryOptions.show_source_data !== false, // デフォルトtrue
198
+ show_total_dose: summaryOptions.show_total_dose !== false // デフォルトtrue
199
+ };
200
+
201
+ // 少なくとも一つはtrueでなければならない
202
+ if (!normalized.show_source_data && !normalized.show_total_dose) {
203
+ logger.warn('Both show_source_data and show_total_dose are false, forcing show_total_dose to true');
204
+ normalized.show_total_dose = true;
205
+ }
206
+
207
+ return normalized;
208
+ }
209
+
210
+ /**
211
+ * 出力ディレクトリの作成と書き込み権限チェック
212
+ * @param {Object} outputFiles 出力ファイル設定
213
+ * @returns {Promise<boolean>} ディレクトリアクセス可能かどうか
214
+ */
215
+ async ensureOutputDirectories(outputFiles = {}) {
216
+ const filePaths = Object.values(outputFiles).filter(Boolean);
217
+
218
+ for (const filePath of filePaths) {
219
+ const dir = path.dirname(filePath);
220
+ try {
221
+ await fs.mkdir(dir, { recursive: true });
222
+ // 書き込み権限をテスト
223
+ const testFile = path.join(dir, '.write_test');
224
+ await fs.writeFile(testFile, '');
225
+ await fs.unlink(testFile);
226
+ } catch (error) {
227
+ logger.error('Output directory access failed', { dir, error: error.message });
228
+ return false;
229
+ }
230
+ }
231
+
232
+ return true;
233
+ }
234
+
235
+ /**
236
+ * poker_cui計算を実行
237
+ * @param {string} yamlFile 入力YAMLファイルパス
238
+ * @param {Object} summaryOptions サマリーオプション
239
+ * @param {Object} outputFiles 出力ファイル設定
240
+ * @param {number} timeout タイムアウト時間(ミリ秒)
241
+ * @returns {Promise<Object>} 計算結果
242
+ */
243
+ async executeCalculation(yamlFile, summaryOptions = {}, outputFiles = {}, timeout = this.defaultTimeout) {
244
+ // 前処理: 各種検証
245
+ logger.info('Starting calculation validation', { yamlFile, summaryOptions, outputFiles });
246
+
247
+ // 1. poker_cui利用可能性チェック
248
+ const isPokerCuiAvailable = await this.checkPokerCuiAvailability();
249
+ if (!isPokerCuiAvailable) {
250
+ throw new CalculationError(
251
+ 'poker_cui command is not available',
252
+ 'POKER_CUI_NOT_AVAILABLE',
253
+ { yamlFile },
254
+ -32042
255
+ );
256
+ }
257
+
258
+ // 2. YAMLファイル完全検証(絶対パス + 存在 + アクセス権限)
259
+ const yamlValidation = await this.validateYamlFile(yamlFile);
260
+ if (!yamlValidation.valid) {
261
+ throw new CalculationError(
262
+ yamlValidation.error,
263
+ 'YAML_FILE_VALIDATION_FAILED',
264
+ {
265
+ yamlFile,
266
+ expectedFormat: '絶対パス形式(例: C:\\path\\to\\file.yaml または /path/to/file.yaml)'
267
+ },
268
+ -32040
269
+ );
270
+ }
271
+
272
+ // 3. YAML構文チェック
273
+ const syntaxCheck = await this.validateYamlSyntax(yamlFile);
274
+ if (!syntaxCheck.valid) {
275
+ throw new CalculationError(
276
+ `Invalid YAML syntax: ${syntaxCheck.error}`,
277
+ 'INVALID_YAML_SYNTAX',
278
+ { yamlFile, syntaxError: syntaxCheck.error },
279
+ -32041
280
+ );
281
+ }
282
+
283
+ // 4. サマリーオプション正規化
284
+ const normalizedSummary = this.validateAndNormalizeSummaryOptions(summaryOptions);
285
+
286
+ // 5. 出力ディレクトリ確保
287
+ const directoriesOk = await this.ensureOutputDirectories(outputFiles);
288
+ if (!directoriesOk) {
289
+ throw new CalculationError(
290
+ 'Output directory access denied or creation failed',
291
+ 'OUTPUT_DIRECTORY_ACCESS_DENIED',
292
+ { outputFiles },
293
+ -32046
294
+ );
295
+ }
296
+
297
+ // コマンドライン引数の構築
298
+ const args = [yamlFile];
299
+
300
+ // サマリーオプション
301
+ if (normalizedSummary.show_parameters) args.push('-p');
302
+ if (normalizedSummary.show_source_data) args.push('-s');
303
+ if (normalizedSummary.show_total_dose) args.push('-t');
304
+
305
+ // 出力ファイル指定
306
+ if (outputFiles.summary_file) {
307
+ args.push('-o', outputFiles.summary_file);
308
+ }
309
+ if (outputFiles.dose_file) {
310
+ args.push('-d', outputFiles.dose_file);
311
+ }
312
+
313
+ logger.info('Executing poker_cui calculation', {
314
+ command: this.pokercuiCommand,
315
+ args,
316
+ timeout
317
+ });
318
+
319
+ // poker_cui実行
320
+ return await this._executePokercui(args, timeout, {
321
+ yamlFile,
322
+ summaryOptions: normalizedSummary,
323
+ outputFiles
324
+ });
325
+ }
326
+
327
+ /**
328
+ * poker_cuiプロセスの実際の実行
329
+ * @param {Array<string>} args コマンドライン引数
330
+ * @param {number} timeout タイムアウト時間
331
+ * @param {Object} context 実行コンテキスト
332
+ * @returns {Promise<Object>} 実行結果
333
+ */
334
+ async _executePokercui(args, timeout, context) {
335
+ return new Promise((resolve, reject) => {
336
+ const startTime = Date.now();
337
+ let isTimedOut = false;
338
+ let isCompleted = false;
339
+
340
+ const child = spawn(this.pokercuiCommand, args, {
341
+ stdio: 'pipe',
342
+ shell: true,
343
+ env: { ...process.env }
344
+ });
345
+
346
+ let stdout = '';
347
+ let stderr = '';
348
+
349
+ // データ収集
350
+ child.stdout.on('data', (data) => {
351
+ const chunk = data.toString();
352
+ stdout += chunk;
353
+ logger.debug('poker_cui stdout chunk', { chunk: chunk.substring(0, 200) });
354
+ });
355
+
356
+ child.stderr.on('data', (data) => {
357
+ const chunk = data.toString();
358
+ stderr += chunk;
359
+ logger.debug('poker_cui stderr chunk', { chunk: chunk.substring(0, 200) });
360
+ });
361
+
362
+ // プロセス終了処理
363
+ child.on('close', (code, signal) => {
364
+ if (isCompleted) return;
365
+ isCompleted = true;
366
+
367
+ const endTime = Date.now();
368
+ const executionTime = endTime - startTime;
369
+
370
+ logger.info('poker_cui execution completed', {
371
+ code,
372
+ signal,
373
+ executionTime,
374
+ stdoutLength: stdout.length,
375
+ stderrLength: stderr.length
376
+ });
377
+
378
+ if (isTimedOut) {
379
+ reject(new CalculationError(
380
+ `Calculation timed out after ${timeout}ms`,
381
+ 'CALCULATION_TIMEOUT',
382
+ { ...context, executionTime, timeout },
383
+ -32044
384
+ ));
385
+ return;
386
+ }
387
+
388
+ if (code !== 0) {
389
+ reject(new CalculationError(
390
+ `poker_cui execution failed with exit code ${code}`,
391
+ 'CALCULATION_EXECUTION_FAILED',
392
+ {
393
+ ...context,
394
+ exitCode: code,
395
+ signal,
396
+ stdout: stdout.substring(0, 1000),
397
+ stderr: stderr.substring(0, 1000),
398
+ executionTime
399
+ },
400
+ -32043
401
+ ));
402
+ return;
403
+ }
404
+
405
+ // 成功時の結果処理
406
+ resolve({
407
+ success: true,
408
+ executionTime,
409
+ outputs: {
410
+ stdout: stdout.trim(),
411
+ stderr: stderr.trim()
412
+ },
413
+ files: this._identifyOutputFiles(context.outputFiles),
414
+ summary: {
415
+ message: 'Calculation completed successfully',
416
+ yamlFile: context.yamlFile,
417
+ options: context.summaryOptions,
418
+ timestamp: new Date().toISOString()
419
+ }
420
+ });
421
+ });
422
+
423
+ // エラー処理
424
+ child.on('error', (error) => {
425
+ if (isCompleted) return;
426
+ isCompleted = true;
427
+
428
+ logger.error('poker_cui process error', { error: error.message, context });
429
+
430
+ if (error.code === 'ENOENT') {
431
+ reject(new CalculationError(
432
+ 'poker_cui command not found',
433
+ 'POKER_CUI_NOT_AVAILABLE',
434
+ { ...context, systemError: error.message },
435
+ -32042
436
+ ));
437
+ } else {
438
+ reject(new CalculationError(
439
+ `Process execution error: ${error.message}`,
440
+ 'CALCULATION_EXECUTION_FAILED',
441
+ { ...context, systemError: error.message },
442
+ -32043
443
+ ));
444
+ }
445
+ });
446
+
447
+ // タイムアウト処理
448
+ const timeoutId = setTimeout(() => {
449
+ if (isCompleted) return;
450
+
451
+ isTimedOut = true;
452
+ logger.warn('poker_cui calculation timeout, killing process', {
453
+ timeout,
454
+ context
455
+ });
456
+
457
+ child.kill('SIGKILL');
458
+ }, timeout);
459
+
460
+ // プロセス完了時にタイマーをクリア
461
+ child.on('close', () => {
462
+ clearTimeout(timeoutId);
463
+ });
464
+ });
465
+ }
466
+
467
+ /**
468
+ * 出力ファイルの存在確認と情報取得
469
+ * @param {Object} outputFiles 出力ファイル設定(絶対パス)
470
+ * @returns {Object} ファイル情報
471
+ */
472
+ _identifyOutputFiles(outputFiles = {}) {
473
+ const fileInfo = {};
474
+
475
+ for (const [key, filePath] of Object.entries(outputFiles)) {
476
+ if (filePath) {
477
+ fileInfo[key] = {
478
+ path: path.basename(filePath), // ファイル名のみを返す(レスポンス用)
479
+ fullPath: filePath, // 内部用の完全パス
480
+ exists: false, // 後で実際にチェックする場合は async にする
481
+ name: path.basename(filePath),
482
+ directory: path.dirname(filePath)
483
+ };
484
+ }
485
+ }
486
+
487
+ return fileInfo;
488
+ }
489
+
490
+ /**
491
+ * 計算結果ファイルの後処理検証
492
+ * @param {Object} outputFiles 出力ファイル設定
493
+ * @returns {Promise<Object>} ファイル検証結果
494
+ */
495
+ async verifyOutputFiles(outputFiles = {}) {
496
+ const verification = {
497
+ summary_file: null,
498
+ dose_file: null,
499
+ allFilesGenerated: true
500
+ };
501
+
502
+ for (const [key, filePath] of Object.entries(outputFiles)) {
503
+ if (filePath) {
504
+ try {
505
+ const stats = await fs.stat(filePath);
506
+ verification[key] = {
507
+ exists: true,
508
+ size: stats.size,
509
+ modified: stats.mtime,
510
+ path: filePath
511
+ };
512
+ } catch (error) {
513
+ verification[key] = {
514
+ exists: false,
515
+ error: error.message,
516
+ path: filePath
517
+ };
518
+ verification.allFilesGenerated = false;
519
+ }
520
+ }
521
+ }
522
+
523
+ return verification;
524
+ }
525
+ }