poker-mcp 1.9.8 → 1.10.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 +175 -5
- package/package.json +6 -2
- package/src/mcp/handlers/calculationHandlers.js +28 -8
- package/src/mcp/handlers/index.js +23 -11
- package/src/mcp/handlers/jobHandlers.js +183 -0
- package/src/mcp/middleware/errorHandler.js +19 -1
- package/src/mcp/server.js +18 -1
- package/src/mcp/tools/index.js +17 -2
- package/src/mcp/tools/jobTools.js +98 -0
- package/src/services/JobManager.js +254 -0
- package/src/utils/asyncConfig.js +34 -0
- package/src/utils/guiConfig.js +40 -0
- package/tools/gen_input.py +717 -0
- package/tools/scatter.py +474 -0
package/README.md
CHANGED
|
@@ -4,14 +4,121 @@ YAML-based input file management tool for radiation-shielding calculation code P
|
|
|
4
4
|
|
|
5
5
|
## 📋 クイック情報
|
|
6
6
|
|
|
7
|
-
- **バージョン**: 1.
|
|
8
|
-
- **ツール数**: 36
|
|
7
|
+
- **バージョン**: 1.10.0
|
|
8
|
+
- **ツール数**: 36メソッド(非同期ジョブを有効にすると 39、GUI を外すと 1 減る)
|
|
9
9
|
- **プロトコル**: MCP (Model Context Protocol) 1.0.0 完全準拠
|
|
10
10
|
- **メインサーバー**: `src/mcp_server_stdio_v4.js`
|
|
11
11
|
- **データ保存**: `~/.poker-mcp/`(`POKER_MCP_HOME`環境変数で変更可)
|
|
12
12
|
- **核種データ**: `${POKER_INSTALL_PATH}/LIB/ICRP-07.NDX` を直接参照
|
|
13
13
|
- **実行方式**: STDIO通信(MCPプロトコル標準)
|
|
14
14
|
|
|
15
|
+
## 🆕 バージョン1.10.0の新機能
|
|
16
|
+
|
|
17
|
+
### ⏳ 計算の非同期実行(既定では無効)
|
|
18
|
+
|
|
19
|
+
時間のかかる計算を「投入して、あとで結果を取りに行く」形で扱えるようにしました。
|
|
20
|
+
1 回のツール呼び出しは数秒で終わるので、MCP クライアントの待ち時間の上限や、
|
|
21
|
+
リモート公開時の中継(Cloudflare Tunnel の約 100 秒など)に切られずに、
|
|
22
|
+
何時間かかる計算でも扱えます。
|
|
23
|
+
|
|
24
|
+
```javascript
|
|
25
|
+
poker_submitCalculation({ yaml_file: "poker.yaml" }) // → { job_id: "20261013-053012-a1b2c3", status: "queued" }
|
|
26
|
+
poker_getJobStatus({ job_id: "20261013-053012-a1b2c3" }) // → queued / running / succeeded / failed
|
|
27
|
+
poker_getJobResult({ job_id: "20261013-053012-a1b2c3" }) // → executeCalculation と同じ形式の結果
|
|
28
|
+
poker_getDoseMap({ detector_name: "D_map", job_id: "20261013-053012-a1b2c3" })
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
**既定では無効です。** 環境変数 `POKER_MCP_ASYNC=1` を設定したときだけ、上の 3 ツールが
|
|
32
|
+
現れ、`poker_getDoseMap` に `job_id` 引数が付きます。設定しなければ、ツール一覧も動作も
|
|
33
|
+
v1.9.9 と同じです。主にリモート公開したサーバで使うことを想定しています。
|
|
34
|
+
|
|
35
|
+
```json
|
|
36
|
+
"env": {
|
|
37
|
+
"POKER_MCP_HOME": "C:\\Users\\<username>\\poker_mcp_workspace",
|
|
38
|
+
"POKER_INSTALL_PATH": "C:/Poker",
|
|
39
|
+
"POKER_MCP_ASYNC": "1"
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
| 環境変数 | 既定 | 内容 |
|
|
44
|
+
|---|---|---|
|
|
45
|
+
| `POKER_MCP_ASYNC` | 無効 | `1` で非同期ジョブを有効にする |
|
|
46
|
+
| `POKER_MCP_ASYNC_CONCURRENCY` | 1 | 同時に実行する計算の数 |
|
|
47
|
+
| `POKER_MCP_JOB_RETENTION_DAYS` | 7 | 終わったジョブを残す日数 |
|
|
48
|
+
| `POKER_MCP_JOB_TIMEOUT_MIN` | 1440 | 1 件の計算の打ち切り時間 [分] |
|
|
49
|
+
|
|
50
|
+
**計算の中身は同期と同じです。** ジョブも `poker_executeCalculation` と同じ処理で POKER を
|
|
51
|
+
呼びます。`npm run test:jobs` で、同期と非同期の `.summary`・`.dose`・線量マップが
|
|
52
|
+
全行一致することを確認しています(生成日時・計算時間・入力ファイルのパスの行を除く)。
|
|
53
|
+
|
|
54
|
+
**投入した時点の入力で計算します。** YAML(と `.paths`)を
|
|
55
|
+
`POKER_MCP_HOME/jobs/<ジョブID>/` にコピーしてから計算し、出力もそこに書きます。
|
|
56
|
+
投入後に作業中の YAML を編集しても、投入済みのジョブには混ざらず、
|
|
57
|
+
作業フォルダの `poker.yaml.summary` なども上書きしません。保留中の変更は含まれないので、
|
|
58
|
+
必要なら先に `poker_applyChanges` で確定してください。
|
|
59
|
+
|
|
60
|
+
**サーバを再起動してもジョブは残ります。** 状態は `job.json` に保存しています。
|
|
61
|
+
実行中に再起動した場合、その計算は失われているため `failed`(`INTERRUPTED_BY_RESTART`)
|
|
62
|
+
になります。待機中だったジョブは再び並び、実行されます。
|
|
63
|
+
|
|
64
|
+
### 🖥️ GUI(poker_openGui)を使える環境でだけ登録
|
|
65
|
+
|
|
66
|
+
`poker_openGui` は、MCP サーバが動いている機械の画面に POKER.exe を開きます。次のどれかに
|
|
67
|
+
当たるときは、ツール一覧から外し、処理も登録しません(名前で直接呼ばれても実行しません)。
|
|
68
|
+
|
|
69
|
+
| 条件 | 想定する場面 |
|
|
70
|
+
|---|---|
|
|
71
|
+
| `POKER_MCP_GUI=0` | **リモート公開したサーバ。** サーバに POKER が入っていても、GUI は利用者の手元ではなくサーバの画面に開き、利用者からは見えないため |
|
|
72
|
+
| Windows 以外 | POKER.exe は Windows 専用 |
|
|
73
|
+
| `POKER_INSTALL_PATH` に POKER.exe が無い | POKER の GUI を入れていない環境 |
|
|
74
|
+
|
|
75
|
+
手元の Windows に POKER を入れて Claude Desktop から使う普段の環境では、従来どおり
|
|
76
|
+
36 ツールが登録されます。判定はサーバの起動時に 1 回だけ行い、結果は起動ログに
|
|
77
|
+
`poker_openGui: 有効(…)` / `無効(…)` と理由付きで出ます。
|
|
78
|
+
|
|
79
|
+
GUI を外しても、計算(POKER_CUI.exe)・検証・CAD 連携・非同期ジョブには影響しません。
|
|
80
|
+
POKER.exe を使うのは `poker_openGui` だけです。
|
|
81
|
+
|
|
82
|
+
リモート公開するサーバの設定例(非同期ジョブを有効にし、GUI を外す):
|
|
83
|
+
|
|
84
|
+
```json
|
|
85
|
+
"env": {
|
|
86
|
+
"POKER_MCP_HOME": "C:\\poker_mcp_workspace",
|
|
87
|
+
"POKER_INSTALL_PATH": "C:/Poker",
|
|
88
|
+
"POKER_MCP_ASYNC": "1",
|
|
89
|
+
"POKER_MCP_GUI": "0"
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## 🐛 バージョン1.9.9の修正
|
|
94
|
+
|
|
95
|
+
npm で入れた環境(`npx poker-mcp`)でだけ現れる不具合と、失敗の伝え方の不具合を
|
|
96
|
+
直しました。まっさらな Windows に npm パッケージから入れた環境で、CAD 連携を含む
|
|
97
|
+
計算結果がリポジトリ上の実行と全行一致することを確認しています。
|
|
98
|
+
|
|
99
|
+
### `poker_generateInput` が npm 版で動かなかった
|
|
100
|
+
|
|
101
|
+
`tools/gen_input.py` と、それが読み込む `tools/scatter.py` が `package.json` の
|
|
102
|
+
`files` から漏れていました。リポジトリから動かすと問題なく、npm から入れた環境で
|
|
103
|
+
だけ `No module named 'gen_input'` で失敗していました。
|
|
104
|
+
|
|
105
|
+
### 失敗が成功として返っていた
|
|
106
|
+
|
|
107
|
+
CAD 系などのハンドラが返す `{ content, isError }` を、共通の処理がもう一度包んで
|
|
108
|
+
いたため、`isError: true` が本文の中に埋もれ、MCP の上では成功として伝わって
|
|
109
|
+
いました。AI クライアントは失敗に気づかないまま次の手順へ進みます。
|
|
110
|
+
|
|
111
|
+
すでに MCP の応答形式になっている戻り値はそのまま返し、`{ success: false, ... }`
|
|
112
|
+
を返すハンドラには `isError: true` を付けるようにしました。立体・ゾーン・線源など
|
|
113
|
+
の検証エラーも、MCP の上で失敗として伝わります。
|
|
114
|
+
|
|
115
|
+
### 計算エラーの処理そのものが落ちていた
|
|
116
|
+
|
|
117
|
+
`poker_executeCalculation` が計算エラーを返す途中で
|
|
118
|
+
`Cannot read properties of undefined (reading '_isRecoverableError')` になり、
|
|
119
|
+
本来のエラー内容が隠れていました。ハンドラがオブジェクトから切り離して呼ばれる
|
|
120
|
+
ため `this` が未定義になっていたのが原因です。
|
|
121
|
+
|
|
15
122
|
## 🆕 バージョン1.9.8の新機能
|
|
16
123
|
|
|
17
124
|
### 📜 `poker_getSchema` — ファイル書式を JSON Schema で取得
|
|
@@ -377,10 +484,27 @@ npx poker-mcp
|
|
|
377
484
|
POKERのインストールディレクトリを指定します。以下の2つの用途で参照されます。
|
|
378
485
|
|
|
379
486
|
- **ICRP-07.NDX の参照先**: `{POKER_INSTALL_PATH}/LIB/ICRP-07.NDX` を直接参照(v1.4.0以降、コピーなし)
|
|
380
|
-
- **POKER.exe の場所**: `{POKER_INSTALL_PATH}/POKER.exe`(`poker_openGui`
|
|
487
|
+
- **POKER.exe の場所**: `{POKER_INSTALL_PATH}/POKER.exe`(`poker_openGui` 使用時)。
|
|
488
|
+
POKER.exe が見つからなければ `poker_openGui` は登録されません(v1.10.0)
|
|
381
489
|
|
|
382
490
|
**デフォルト値**: `C:/Poker`(未設定時は `C:/Poker` を使用)
|
|
383
491
|
|
|
492
|
+
#### `POKER_MCP_ASYNC` ほか(オプション、v1.10.0)
|
|
493
|
+
計算の非同期実行(`poker_submitCalculation` など)を有効にします。既定では無効で、設定しなければ
|
|
494
|
+
ツール一覧も動作も変わりません。詳しくは「[バージョン1.10.0の新機能](#-バージョン1100の新機能)」。
|
|
495
|
+
|
|
496
|
+
| 変数 | 既定 | 内容 |
|
|
497
|
+
|---|---|---|
|
|
498
|
+
| `POKER_MCP_ASYNC` | 無効 | `1` で非同期ジョブを有効にする |
|
|
499
|
+
| `POKER_MCP_ASYNC_CONCURRENCY` | 1 | 同時に実行する計算の数 |
|
|
500
|
+
| `POKER_MCP_JOB_RETENTION_DAYS` | 7 | 終わったジョブを残す日数 |
|
|
501
|
+
| `POKER_MCP_JOB_TIMEOUT_MIN` | 1440 | 1 件の計算の打ち切り時間 [分] |
|
|
502
|
+
|
|
503
|
+
#### `POKER_MCP_GUI`(オプション、v1.10.0)
|
|
504
|
+
`0` を設定すると `poker_openGui` を登録しません。リモート公開するサーバで使います
|
|
505
|
+
(GUI がサーバの画面に開き、利用者からは見えないため)。Windows 以外の環境や、
|
|
506
|
+
POKER.exe が見つからない環境では、設定しなくても登録されません。計算には影響しません。
|
|
507
|
+
|
|
384
508
|
```bash
|
|
385
509
|
# Windows(コマンドプロンプト)
|
|
386
510
|
set POKER_MCP_HOME=C:\Users\yoshi\poker_mcp_workspace
|
|
@@ -402,6 +526,7 @@ POKER_MCP_HOME/ # デフォルト: ~/.poker-mcp/
|
|
|
402
526
|
├── backups/ # 自動バックアップ(最大10世代)
|
|
403
527
|
├── data/ # ICRP-07.NDX 核種データベース
|
|
404
528
|
├── logs/ # error.log, combined.log
|
|
529
|
+
├── jobs/ # 非同期ジョブ(POKER_MCP_ASYNC=1 のときだけ。既定 7 日で自動削除)
|
|
405
530
|
└── config.json # ユーザー設定(任意)
|
|
406
531
|
```
|
|
407
532
|
|
|
@@ -437,6 +562,13 @@ POKER_MCP_HOME/ # デフォルト: ~/.poker-mcp/
|
|
|
437
562
|
> **⚠️ 注意:** `cwd`(作業ディレクトリ)の指定は不要です。`POKER_MCP_HOME`
|
|
438
563
|
> 環境変数で管理するため、`cwd` を設定すると v1.2.5 以前の問題が再発します。
|
|
439
564
|
|
|
565
|
+
> **版の固定:** `npx` は一度取得した版を使い続けることがあります。確実に特定の版を
|
|
566
|
+
> 使うには `"args": ["poker-mcp@1.9.9"]` のように版を書きます。v1.9.8 以前の npm 版では
|
|
567
|
+
> `poker_generateInput` が動きません(v1.9.9 で修正)。
|
|
568
|
+
|
|
569
|
+
リモート公開するサーバ向けの設定(非同期ジョブを有効にし、GUI を外す)は
|
|
570
|
+
「[バージョン1.10.0の新機能](#-バージョン1100の新機能)」を参照してください。
|
|
571
|
+
|
|
440
572
|
3. **Claude Desktopを再起動** してMCPサーバーを有効化
|
|
441
573
|
|
|
442
574
|
### 4. 動作確認
|
|
@@ -456,7 +588,9 @@ Claude Desktopで以下のようにテストできます:
|
|
|
456
588
|
## 🏆 主要機能
|
|
457
589
|
|
|
458
590
|
### ✅ **MCP完全対応**
|
|
459
|
-
- **36メソッド完全実装**:
|
|
591
|
+
- **36メソッド完全実装**: 全ての放射線遮蔽計算入力管理機能(非同期ジョブ有効時は 39)
|
|
592
|
+
- **失敗の通知**: ツールが処理の結果として失敗(`success: false`)を返すと、MCP の `isError: true` が付く(v1.9.9)
|
|
593
|
+
- **計算の非同期実行**: 長い計算を投入して後から結果を取得(`POKER_MCP_ASYNC=1`、v1.10.0)
|
|
460
594
|
- **JSON-RPC 2.0準拠**: 標準プロトコル完全対応
|
|
461
595
|
- **STDIO通信**: MCPクライアントとの標準通信方式
|
|
462
596
|
- **自動バックアップ・ロールバック**: 企業品質のデータ保護
|
|
@@ -490,6 +624,10 @@ Claude Desktopで以下のようにテストできます:
|
|
|
490
624
|
| **📐 CAD** | 2個 | CAD から入力と経路を生成 | generateInput・generatePaths |
|
|
491
625
|
| **⚙️ System** | 6個 | システム制御 | applyChanges・executeCalculation・resetYaml・confirmDaughterNuclides・openGui・getDoseMap |
|
|
492
626
|
| **📜 Schema** | 1個 | ファイル書式の取得 | getSchema |
|
|
627
|
+
| **⏳ Job**(`POKER_MCP_ASYNC=1` のときだけ) | 3個 | 計算の非同期実行 | submitCalculation・getJobStatus・getJobResult |
|
|
628
|
+
|
|
629
|
+
- `poker_openGui` は、Windows で POKER.exe が見つかり、`POKER_MCP_GUI=0` でないときだけ登録されます。
|
|
630
|
+
- 非同期ジョブを有効にすると、`poker_getDoseMap` にジョブを指定する `job_id` 引数が付きます。
|
|
493
631
|
|
|
494
632
|
### 📋 **全36メソッド一覧**
|
|
495
633
|
```
|
|
@@ -508,6 +646,9 @@ CAD系 (2): poker_generateInput, poker_generatePaths
|
|
|
508
646
|
System系 (6): poker_applyChanges, poker_executeCalculation, poker_resetYaml,
|
|
509
647
|
poker_confirmDaughterNuclides, poker_openGui, poker_getDoseMap
|
|
510
648
|
Schema系 (1): poker_getSchema
|
|
649
|
+
|
|
650
|
+
(POKER_MCP_ASYNC=1 のとき追加)
|
|
651
|
+
Job系 (3): poker_submitCalculation, poker_getJobStatus, poker_getJobResult
|
|
511
652
|
```
|
|
512
653
|
|
|
513
654
|
## 📁 プロジェクト構造
|
|
@@ -542,6 +683,8 @@ POKER_MCP_HOME/
|
|
|
542
683
|
├── 📁 logs/ # 📝 ログファイル
|
|
543
684
|
│ ├── error.log
|
|
544
685
|
│ └── combined.log
|
|
686
|
+
├── 📁 jobs/ # ⏳ 非同期ジョブ(POKER_MCP_ASYNC=1 のときだけ)
|
|
687
|
+
│ └── <ジョブID>/ # job.json・入力のコピー・.summary・.dose・stdout/stderr
|
|
545
688
|
└── config.json # ユーザー設定(任意)
|
|
546
689
|
```
|
|
547
690
|
|
|
@@ -635,6 +778,22 @@ POKER_MCP_HOME/
|
|
|
635
778
|
- **メモリ**: 512MB以上推奨(大規模検出器使用時は1GB以上)
|
|
636
779
|
- **POKER 本体**: v2.1.5 以降(`POKER_INSTALL_PATH` で場所を指定)
|
|
637
780
|
- **FreeCAD**: 1.0 以降(CAD連携を使う場合。`FREECAD_PATH`、未設定なら自動探索)
|
|
781
|
+
- **POKER の GUI(POKER.exe)**: `poker_openGui` を使う場合のみ。Windows 専用
|
|
782
|
+
|
|
783
|
+
## 🧪 開発者向けの確認
|
|
784
|
+
|
|
785
|
+
リポジトリで次を実行します。`check:manifest` 以外は POKER 本体が必要です(`POKER_INSTALL_PATH` 既定 `C:/Poker`)。
|
|
786
|
+
|
|
787
|
+
| コマンド | 内容 |
|
|
788
|
+
|---|---|
|
|
789
|
+
| `npm run check:manifest` | ツール定義とマニフェストの整合。既定・非同期有効・非同期有効かつ GUI 無効の 3 通り |
|
|
790
|
+
| `npm run test:jobs` | 非同期ジョブ。同期との全行一致、投入後の編集が混ざらないこと、待ち行列、再起動後の復元 |
|
|
791
|
+
| `npm run test:gui` | `poker_openGui` の登録条件(`POKER_MCP_GUI=0` で外れ、計算は従来どおり動く) |
|
|
792
|
+
| `npm run test:schema` | `poker_getSchema` と `.dose` の読み取り |
|
|
793
|
+
| `npm run test:pending` | 保留中の変更の表示 |
|
|
794
|
+
| `npm run test:cad-validate` など | CAD 連携(FreeCAD が必要) |
|
|
795
|
+
|
|
796
|
+
`test:jobs` と `test:gui` は、失敗があれば終了コード 1 で終わります。
|
|
638
797
|
|
|
639
798
|
## 🎯 実際の使用ワークフロー
|
|
640
799
|
|
|
@@ -659,6 +818,17 @@ POKER_MCP_HOME/
|
|
|
659
818
|
|
|
660
819
|
## 📝 更新履歴
|
|
661
820
|
|
|
821
|
+
### v1.10.0 (2026-10)
|
|
822
|
+
- ✨ 計算の非同期実行(`poker_submitCalculation` / `poker_getJobStatus` / `poker_getJobResult`、`poker_getDoseMap` の `job_id`)。`POKER_MCP_ASYNC=1` のときだけ有効
|
|
823
|
+
- ✨ `poker_openGui` を GUI が使える環境でだけ登録(`POKER_MCP_GUI=0`、Windows 以外、POKER.exe なしで外す)
|
|
824
|
+
- ✅ `npm run test:jobs`(同期との全行一致、入力のスナップショット、再起動後の復元)と `npm run test:gui` を追加
|
|
825
|
+
- 🔧 `npm run check:manifest` が非同期ジョブ有効時・GUI 無効時のツール定義も照合
|
|
826
|
+
|
|
827
|
+
### v1.9.9 (2026-10)
|
|
828
|
+
- 🐛 `tools/gen_input.py` と `tools/scatter.py` を npm パッケージに同梱(`poker_generateInput` が npm 版で動かなかった)
|
|
829
|
+
- 🐛 ツールの失敗が `isError: true` として伝わるよう修正(応答の二重包みを解消、`success: false` に `isError` を付与)
|
|
830
|
+
- 🐛 `poker_executeCalculation` の計算エラー処理が `this` 未定義で落ちていた問題を修正
|
|
831
|
+
|
|
662
832
|
### v1.9.8 (2026-10)
|
|
663
833
|
- ✨ `poker_getSchema`(入力YAML・`.paths`・`.summary`・`.dose` の書式を JSON Schema で取得)
|
|
664
834
|
- 🐛 `poker_getDoseMap` が `.dose` の列構成を `information` から読むようになり、線量種別の増加に追随
|
|
@@ -749,6 +919,6 @@ obtained separately and licensed separately. poker-mcp は `poker_cui` を外部
|
|
|
749
919
|
|
|
750
920
|
---
|
|
751
921
|
|
|
752
|
-
**🎯 Poker MCP Server v1.
|
|
922
|
+
**🎯 Poker MCP Server v1.10.0**
|
|
753
923
|
**プロトコル**: MCP 1.0.0 完全準拠
|
|
754
924
|
**作者**: Yoshihiro Hirao | **ライセンス**: MIT
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "poker-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.10.0",
|
|
4
4
|
"description": "YAML-based input file management tool for radiation shielding calculation code POKER with MCP support - Enhanced with collision detection, daughter nuclide auto-completion, and environment variable support",
|
|
5
5
|
"main": "src/mcp_server_stdio_v4.js",
|
|
6
6
|
"type": "module",
|
|
@@ -11,7 +11,9 @@
|
|
|
11
11
|
"start": "node src/mcp_server_stdio_v4.js",
|
|
12
12
|
"dev": "nodemon src/mcp_server_stdio_v4.js",
|
|
13
13
|
"prepare": "echo 'NPX ready: npx poker-mcp'",
|
|
14
|
-
"check:manifest": "node scripts/check-manifest-sync.mjs",
|
|
14
|
+
"check:manifest": "node scripts/check-manifest-sync.mjs && node scripts/check-manifest-sync.mjs --async && node scripts/check-manifest-sync.mjs --async --no-gui",
|
|
15
|
+
"test:jobs": "node tests/async-jobs.test.mjs",
|
|
16
|
+
"test:gui": "node tests/gui-switch.test.mjs",
|
|
15
17
|
"test:pending": "node tests/pending-view.test.mjs",
|
|
16
18
|
"test:cad": "node tests/cad-e2e.test.mjs",
|
|
17
19
|
"test:cad-full": "node tests/cad-full.test.mjs",
|
|
@@ -56,7 +58,9 @@
|
|
|
56
58
|
},
|
|
57
59
|
"files": [
|
|
58
60
|
"src/",
|
|
61
|
+
"tools/gen_input.py",
|
|
59
62
|
"tools/gen_paths.py",
|
|
63
|
+
"tools/scatter.py",
|
|
60
64
|
"tools/ray_trace_tri.py",
|
|
61
65
|
"tools/poker_lib.py",
|
|
62
66
|
"tools/audit_mfp.py",
|
|
@@ -7,19 +7,37 @@ import { TASKS_DIR } from '../../utils/paths.js';
|
|
|
7
7
|
import { parseDoseSummary } from '../../utils/summaryParser.js';
|
|
8
8
|
import { parseDoseMap } from '../../utils/doseMapParser.js';
|
|
9
9
|
|
|
10
|
-
export function createCalculationHandlers(taskManager) {
|
|
10
|
+
export function createCalculationHandlers(taskManager, { jobManager = null } = {}) {
|
|
11
11
|
const calculationService = new CalculationService();
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
// server.js はハンドラを handler(args) の形でオブジェクトから切り離して呼ぶため、
|
|
14
|
+
// 内部の補助関数は this ではなくこの変数経由で参照する。
|
|
15
|
+
const handlers = {
|
|
14
16
|
async getDoseMap(args) {
|
|
15
17
|
try {
|
|
16
18
|
if (!args.detector_name) {
|
|
17
19
|
throw new ValidationError('detector_name is required', 'detector_name', args.detector_name);
|
|
18
20
|
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
let dosePath;
|
|
22
|
+
if (args.job_id) {
|
|
23
|
+
// 非同期ジョブの結果を読む(ジョブ専用フォルダの .dose)
|
|
24
|
+
if (!jobManager) {
|
|
25
|
+
return { success: false, error: 'job_id は非同期ジョブが有効なとき(POKER_MCP_ASYNC=1)だけ使えます' };
|
|
26
|
+
}
|
|
27
|
+
const job = jobManager.get(args.job_id);
|
|
28
|
+
if (!job) {
|
|
29
|
+
return { success: false, error: `ジョブが見つかりません: ${args.job_id}`, error_type: 'JOB_NOT_FOUND' };
|
|
30
|
+
}
|
|
31
|
+
if (job.status !== 'succeeded') {
|
|
32
|
+
return { success: false, error: `ジョブ ${args.job_id} は完了していません(status: ${job.status})`, status: job.status };
|
|
33
|
+
}
|
|
34
|
+
dosePath = job.outputs.dose_file;
|
|
35
|
+
} else {
|
|
36
|
+
const yamlFile = args.yaml_file || 'poker.yaml';
|
|
37
|
+
const absolutePathPattern = /^([a-zA-Z]:[\\\/]|\/)/;
|
|
38
|
+
const resolvedYamlFile = absolutePathPattern.test(yamlFile) ? yamlFile : path.join(TASKS_DIR, yamlFile);
|
|
39
|
+
dosePath = resolvedYamlFile + '.dose';
|
|
40
|
+
}
|
|
23
41
|
// .dose は poker_cui 完了直後に書き込まれるが、完了検知からディスク反映までに数秒の
|
|
24
42
|
// ばらつきがある。実運用(executeCalculation とは別コール)では準備済みだが、直後呼び出しに
|
|
25
43
|
// 備え、準備できるまで最大約10秒リトライする(未準備時のみ待機)。
|
|
@@ -231,8 +249,8 @@ export function createCalculationHandlers(taskManager) {
|
|
|
231
249
|
mcp_error_code: error.mcpErrorCode,
|
|
232
250
|
details: {
|
|
233
251
|
category: 'calculation_error',
|
|
234
|
-
recoverable:
|
|
235
|
-
suggestions:
|
|
252
|
+
recoverable: handlers._isRecoverableError(error.code),
|
|
253
|
+
suggestions: handlers._getErrorSuggestions(error.code, error.context)
|
|
236
254
|
}
|
|
237
255
|
};
|
|
238
256
|
}
|
|
@@ -343,4 +361,6 @@ export function createCalculationHandlers(taskManager) {
|
|
|
343
361
|
}
|
|
344
362
|
}
|
|
345
363
|
};
|
|
364
|
+
|
|
365
|
+
return handlers;
|
|
346
366
|
}
|
|
@@ -5,16 +5,25 @@ import { createTransformHandlers } from './transformHandlers.js';
|
|
|
5
5
|
import { createBuildupFactorHandlers } from './buildupFactorHandlers.js';
|
|
6
6
|
import { createSourceHandlers } from './sourceHandlers.js';
|
|
7
7
|
import { createDetectorHandlers } from './detectorHandlers.js';
|
|
8
|
-
import { createUnitHandlers } from './unitHandlers.js';
|
|
9
|
-
import { createThinnedIndicesHandlers } from './thinnedIndicesHandlers.js';
|
|
8
|
+
import { createUnitHandlers } from './unitHandlers.js';
|
|
9
|
+
import { createThinnedIndicesHandlers } from './thinnedIndicesHandlers.js';
|
|
10
10
|
import { createCadHandlers } from './cadHandlers.js';
|
|
11
11
|
import { createCalculationHandlers } from './calculationHandlers.js';
|
|
12
12
|
import { resetHandlers } from './resetHandlers.js';
|
|
13
13
|
import { createDaughterNuclideHandler } from './daughterNuclideHandler.js';
|
|
14
|
-
import { createGuiHandlers } from './guiHandlers.js';
|
|
14
|
+
import { createGuiHandlers } from './guiHandlers.js';
|
|
15
15
|
import { createSchemaHandlers } from './schemaHandlers.js';
|
|
16
|
+
import { createJobHandlers } from './jobHandlers.js';
|
|
16
17
|
|
|
17
|
-
|
|
18
|
+
/**
|
|
19
|
+
* @param {Object} taskManager
|
|
20
|
+
* @param {Object} [opts]
|
|
21
|
+
* @param {import('../../services/JobManager.js').JobManager|null} [opts.jobManager]
|
|
22
|
+
* 非同期ジョブが有効なときだけ渡される。null ならジョブ系のハンドラは登録しない。
|
|
23
|
+
* @param {boolean} [opts.guiEnabled=true]
|
|
24
|
+
* false なら poker_openGui のハンドラを登録しない(ツール名で直接呼ばれても実行しない)。
|
|
25
|
+
*/
|
|
26
|
+
export function createAllHandlers(taskManager, { jobManager = null, guiEnabled = true } = {}) {
|
|
18
27
|
return {
|
|
19
28
|
// 立体操作
|
|
20
29
|
...createBodyHandlers(taskManager),
|
|
@@ -35,12 +44,15 @@ export function createAllHandlers(taskManager) {
|
|
|
35
44
|
...createDetectorHandlers(taskManager),
|
|
36
45
|
|
|
37
46
|
// 単位操作
|
|
38
|
-
...createUnitHandlers(taskManager),
|
|
39
|
-
...createThinnedIndicesHandlers(taskManager),
|
|
47
|
+
...createUnitHandlers(taskManager),
|
|
48
|
+
...createThinnedIndicesHandlers(taskManager),
|
|
40
49
|
...createCadHandlers(taskManager),
|
|
41
50
|
|
|
42
51
|
// 計算操作
|
|
43
|
-
...createCalculationHandlers(taskManager),
|
|
52
|
+
...createCalculationHandlers(taskManager, { jobManager }),
|
|
53
|
+
|
|
54
|
+
// 非同期ジョブ(POKER_MCP_ASYNC=1 のときだけ)
|
|
55
|
+
...(jobManager ? createJobHandlers(taskManager, jobManager) : {}),
|
|
44
56
|
|
|
45
57
|
// 子孫核種確認操作
|
|
46
58
|
confirmDaughterNuclides: createDaughterNuclideHandler(taskManager),
|
|
@@ -48,10 +60,10 @@ export function createAllHandlers(taskManager) {
|
|
|
48
60
|
// リセット操作
|
|
49
61
|
resetYaml: resetHandlers.resetYaml(taskManager),
|
|
50
62
|
|
|
51
|
-
// GUI
|
|
52
|
-
...createGuiHandlers(taskManager),
|
|
53
|
-
|
|
54
|
-
// スキーマ取得(poker_cui --schema)
|
|
63
|
+
// GUI表示操作(Windows で POKER.exe があり、POKER_MCP_GUI=0 でないときだけ)
|
|
64
|
+
...(guiEnabled ? createGuiHandlers(taskManager) : {}),
|
|
65
|
+
|
|
66
|
+
// スキーマ取得(poker_cui --schema)
|
|
55
67
|
...createSchemaHandlers(),
|
|
56
68
|
|
|
57
69
|
// 共通操作
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
// mcp/handlers/jobHandlers.js - 計算の非同期実行のハンドラ
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { ValidationError } from '../../utils/errors.js';
|
|
5
|
+
import { logger } from '../../utils/logger.js';
|
|
6
|
+
import { TASKS_DIR } from '../../utils/paths.js';
|
|
7
|
+
import { parseDoseSummary } from '../../utils/summaryParser.js';
|
|
8
|
+
import { TERMINAL_STATUSES } from '../../services/JobManager.js';
|
|
9
|
+
|
|
10
|
+
const ABSOLUTE = /^([a-zA-Z]:[\\\/]|\/)/;
|
|
11
|
+
const resolveTaskPath = (p) => (ABSOLUTE.test(p) ? p : path.join(TASKS_DIR, p));
|
|
12
|
+
|
|
13
|
+
export function createJobHandlers(taskManager, jobManager) {
|
|
14
|
+
const statusView = (job) => {
|
|
15
|
+
const now = Date.now();
|
|
16
|
+
const start = job.started_at ? Date.parse(job.started_at) : null;
|
|
17
|
+
const end = job.finished_at ? Date.parse(job.finished_at) : null;
|
|
18
|
+
return {
|
|
19
|
+
job_id: job.id,
|
|
20
|
+
status: job.status,
|
|
21
|
+
submitted_at: job.submitted_at,
|
|
22
|
+
started_at: job.started_at,
|
|
23
|
+
finished_at: job.finished_at,
|
|
24
|
+
queue_position: jobManager.queuePosition(job.id),
|
|
25
|
+
running_seconds: start ? Math.round(((end || now) - start) / 1000) : null,
|
|
26
|
+
input_file: job.input.yaml_source,
|
|
27
|
+
path_input: job.input.path_input_source,
|
|
28
|
+
...(job.status === 'failed' ? { error: job.error } : {})
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
async submitCalculation(args) {
|
|
34
|
+
if (!args.yaml_file) {
|
|
35
|
+
throw new ValidationError('YAML file is required', 'yaml_file', args.yaml_file);
|
|
36
|
+
}
|
|
37
|
+
const yamlFile = resolveTaskPath(args.yaml_file);
|
|
38
|
+
if (!/\.(yaml|yml)$/i.test(yamlFile)) {
|
|
39
|
+
throw new ValidationError('YAML file must have .yaml or .yml extension', 'yaml_file', args.yaml_file);
|
|
40
|
+
}
|
|
41
|
+
if (!fs.existsSync(yamlFile)) {
|
|
42
|
+
throw new ValidationError(`YAML file not found: ${yamlFile}`, 'yaml_file', args.yaml_file);
|
|
43
|
+
}
|
|
44
|
+
let pathInput = null;
|
|
45
|
+
if (args.path_input) {
|
|
46
|
+
pathInput = resolveTaskPath(args.path_input);
|
|
47
|
+
if (!fs.existsSync(pathInput)) {
|
|
48
|
+
throw new ValidationError(`path_input not found: ${pathInput}`, 'path_input', args.path_input);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
const outputFiles = args.output_files || {};
|
|
52
|
+
for (const [key, name] of Object.entries(outputFiles)) {
|
|
53
|
+
if (name && !/^[a-zA-Z0-9_\-\.]+\.(yaml|yml)$/.test(name)) {
|
|
54
|
+
throw new ValidationError(`Output file ${key} must be a file name ending with .yaml or .yml`, `output_files.${key}`, name);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// 同期の executeCalculation と同じ事前検証を、投入の時点で行う。
|
|
59
|
+
// 重大エラー(立体干渉を除く)があれば受け付けない。
|
|
60
|
+
if (taskManager?.dataManager) {
|
|
61
|
+
const v = await taskManager.dataManager.performPreCalculationValidation();
|
|
62
|
+
const critical = (v.criticalErrors || []).filter((e) => e.type !== 'collision_detected');
|
|
63
|
+
if (critical.length > 0) {
|
|
64
|
+
return {
|
|
65
|
+
success: false,
|
|
66
|
+
error: 'CRITICAL_VALIDATION_ERRORS',
|
|
67
|
+
error_type: 'PRE_VALIDATION_FAILED',
|
|
68
|
+
message: '計算実行前に解決が必要な重大エラーがあるため、ジョブを受け付けませんでした',
|
|
69
|
+
critical_errors: critical
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const job = await jobManager.submit({
|
|
75
|
+
yamlFile,
|
|
76
|
+
pathInput,
|
|
77
|
+
summaryOptions: args.summary_options || {},
|
|
78
|
+
outputFiles
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
success: true,
|
|
83
|
+
message: '計算を受け付けました。poker_getJobStatus で進み具合を確認し、終わったら poker_getJobResult で結果を取得してください。',
|
|
84
|
+
job_id: job.id,
|
|
85
|
+
status: job.status,
|
|
86
|
+
queue_position: jobManager.queuePosition(job.id),
|
|
87
|
+
input: {
|
|
88
|
+
yaml_file: yamlFile,
|
|
89
|
+
yaml_sha256: job.input.yaml_sha256,
|
|
90
|
+
path_input: pathInput
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
},
|
|
94
|
+
|
|
95
|
+
async getJobStatus(args = {}) {
|
|
96
|
+
if (!args.job_id) {
|
|
97
|
+
const jobs = jobManager.list(20).map(statusView);
|
|
98
|
+
return {
|
|
99
|
+
success: true,
|
|
100
|
+
running: jobManager.running.size,
|
|
101
|
+
queued: jobManager.queue.length,
|
|
102
|
+
concurrency: jobManager.concurrency,
|
|
103
|
+
jobs
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
const job = jobManager.get(args.job_id);
|
|
107
|
+
if (!job) {
|
|
108
|
+
return { success: false, error: `ジョブが見つかりません: ${args.job_id}`, error_type: 'JOB_NOT_FOUND' };
|
|
109
|
+
}
|
|
110
|
+
return { success: true, ...statusView(job) };
|
|
111
|
+
},
|
|
112
|
+
|
|
113
|
+
async getJobResult(args) {
|
|
114
|
+
const job = jobManager.get(args.job_id);
|
|
115
|
+
if (!job) {
|
|
116
|
+
return { success: false, error: `ジョブが見つかりません: ${args.job_id}`, error_type: 'JOB_NOT_FOUND' };
|
|
117
|
+
}
|
|
118
|
+
if (!TERMINAL_STATUSES.has(job.status)) {
|
|
119
|
+
return {
|
|
120
|
+
success: true,
|
|
121
|
+
ready: false,
|
|
122
|
+
...statusView(job),
|
|
123
|
+
message: 'まだ終わっていません。しばらくおいて poker_getJobStatus で確認してください。'
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
if (job.status === 'failed') {
|
|
127
|
+
return {
|
|
128
|
+
success: false,
|
|
129
|
+
ready: true,
|
|
130
|
+
...statusView(job),
|
|
131
|
+
error: job.error?.message,
|
|
132
|
+
error_type: job.error?.code || 'JOB_FAILED',
|
|
133
|
+
details: job.error
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const files = {};
|
|
138
|
+
for (const [key, p] of Object.entries(job.outputs)) {
|
|
139
|
+
try {
|
|
140
|
+
const st = fs.statSync(p);
|
|
141
|
+
files[key] = { exists: true, size: st.size, path: p };
|
|
142
|
+
} catch (e) {
|
|
143
|
+
files[key] = { exists: false, path: p };
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
const stdout = jobManager.readOutput(job.id, 'stdout.txt');
|
|
147
|
+
const stderr = jobManager.readOutput(job.id, 'stderr.txt');
|
|
148
|
+
|
|
149
|
+
const response = {
|
|
150
|
+
success: true,
|
|
151
|
+
ready: true,
|
|
152
|
+
...statusView(job),
|
|
153
|
+
message: 'Radiation shielding calculation completed successfully',
|
|
154
|
+
calculation: {
|
|
155
|
+
input_file: job.input.yaml_source,
|
|
156
|
+
snapshot_file: job.input.yaml_file,
|
|
157
|
+
yaml_sha256: job.input.yaml_sha256,
|
|
158
|
+
execution_time_ms: job.execution_time_ms,
|
|
159
|
+
options: job.summary_options
|
|
160
|
+
},
|
|
161
|
+
outputs: {
|
|
162
|
+
console: { stdout_length: stdout.length, stderr_length: stderr.length },
|
|
163
|
+
files
|
|
164
|
+
},
|
|
165
|
+
details: { stdout, stderr }
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
try {
|
|
169
|
+
const parsed = parseDoseSummary(job.outputs.summary_file);
|
|
170
|
+
if (parsed) {
|
|
171
|
+
response.result_total = parsed.result_total;
|
|
172
|
+
response.dose_columns = parsed.columns;
|
|
173
|
+
if (parsed.notes && parsed.notes.length) response.calculation_notes = parsed.notes;
|
|
174
|
+
if (parsed.elapsed_time) response.calculation.elapsed_time = parsed.elapsed_time;
|
|
175
|
+
if (parsed.warnings && parsed.warnings.length) response.calculation_warnings = parsed.warnings;
|
|
176
|
+
}
|
|
177
|
+
} catch (e) {
|
|
178
|
+
logger.warn('summary 構造化パースをスキップ', { job: job.id, error: e.message });
|
|
179
|
+
}
|
|
180
|
+
return response;
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
}
|
|
@@ -37,11 +37,29 @@ export function handleMcpError(error, context = {}) {
|
|
|
37
37
|
return new McpError(ErrorCode.InternalError, `内部エラー: ${error.message}`);
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
/**
|
|
41
|
+
* ハンドラの戻り値を MCP の CallToolResult に整える。
|
|
42
|
+
* - すでに CallToolResult の形(content 配列を持つ)ならそのまま返す。
|
|
43
|
+
* 包み直すと isError が text の中に埋もれ、失敗が成功として伝わってしまう。
|
|
44
|
+
* - { success: false, ... } を返すハンドラは失敗として isError: true を付ける。
|
|
45
|
+
* AI クライアントは本文を読まずに isError で成否を判断することがあるため。
|
|
46
|
+
*/
|
|
47
|
+
export function toCallToolResult(result) {
|
|
48
|
+
if (result && typeof result === 'object' && Array.isArray(result.content)) {
|
|
49
|
+
return result;
|
|
50
|
+
}
|
|
51
|
+
const wrapped = { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
52
|
+
if (result && typeof result === 'object' && result.success === false) {
|
|
53
|
+
wrapped.isError = true;
|
|
54
|
+
}
|
|
55
|
+
return wrapped;
|
|
56
|
+
}
|
|
57
|
+
|
|
40
58
|
export function safeExecute(fn, context = {}) {
|
|
41
59
|
return async (request) => {
|
|
42
60
|
try {
|
|
43
61
|
const result = await fn(request);
|
|
44
|
-
return
|
|
62
|
+
return toCallToolResult(result);
|
|
45
63
|
} catch (error) {
|
|
46
64
|
throw handleMcpError(error, context);
|
|
47
65
|
}
|