poker-mcp 1.8.2 → 1.8.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "poker-mcp",
3
- "version": "1.8.2",
3
+ "version": "1.8.3",
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",
@@ -12,7 +12,8 @@
12
12
  "dev": "nodemon src/mcp_server_stdio_v4.js",
13
13
  "prepare": "echo 'NPX ready: npx poker-mcp'",
14
14
  "check:manifest": "node scripts/check-manifest-sync.mjs",
15
- "test:pending": "node tests/pending-view.test.mjs"
15
+ "test:pending": "node tests/pending-view.test.mjs",
16
+ "test:cad": "node tests/cad-e2e.test.mjs"
16
17
  },
17
18
  "keywords": [
18
19
  "mcp",
@@ -1,187 +1,194 @@
1
- // mcp/handlers/cadHandlers.js
2
- //
3
- // CAD 連携レイトレース。従来は次の 3 手順を手で行う必要があった。
4
- // 1. spec.json を手書き(絶対パスを 4 箇所)
5
- // 2. poker_cui -p で分割点と評価点を出す
6
- // 3. freecadcmd を起動して gen_paths.py を走らせる
7
- // このハンドラがまとめて行う。
8
-
9
- import { spawn } from 'child_process';
10
- import fs from 'fs/promises';
11
- import fssync from 'fs';
12
- import path from 'path';
13
- import { FREECAD_CMD, CAD_TOOLS_DIR, POKER_INSTALL_DIR, TASKS_DIR, YAML_FILE }
14
- from '../../utils/paths.js';
15
- import { logger } from '../../utils/logger.js';
16
- import { MaterialCatalog } from '../../utils/MaterialCatalog.js';
17
-
18
- function run(cmd, args, opts = {}) {
19
- return new Promise((resolve) => {
20
- const p = spawn(cmd, args, { ...opts, stdio: ['ignore', 'pipe', 'pipe'] });
21
- let out = '', err = '';
22
- p.stdout.on('data', d => out += d);
23
- p.stderr.on('data', d => err += d);
24
- p.on('error', e => resolve({ code: -1, out, err: e.message }));
25
- p.on('close', code => resolve({ code, out, err }));
26
- });
27
- }
28
-
29
- export function createCadHandlers(taskManager) {
30
- return {
31
- async generatePaths(args) {
32
- const fail = (msg, hint) => ({
33
- content: [{
34
- type: 'text',
35
- text: JSON.stringify({ success: false, error: msg, hint }, null, 2)
36
- }],
37
- isError: true
38
- });
39
-
40
- // --- FreeCAD の所在 ---
41
- if (!FREECAD_CMD) {
42
- return fail(
43
- 'FreeCAD が見つかりません',
44
- '環境変数 FREECAD_PATH に freecadcmd の場所を設定してください。' +
45
- '実行ファイル(C:/Program Files/FreeCAD 1.1/bin/freecadcmd.exe)でも、' +
46
- 'インストールフォルダ(C:/Program Files/FreeCAD 1.1)でも指定できます。' +
47
- 'claude_desktop_config.json の env に追加します。');
48
- }
49
- if (!args || !args.fcstd) return fail('fcstd を指定してください');
50
- if (!fssync.existsSync(args.fcstd))
51
- return fail('CAD ファイルが見つかりません: ' + args.fcstd);
52
-
53
- const yamlFile = YAML_FILE;
54
- if (!fssync.existsSync(yamlFile))
55
- return fail('入力 YAML がありません: ' + yamlFile,
56
- 'applyChanges で入力を確定してから実行してください。');
57
-
58
- // --- 1. poker_cui -p で分割点と評価点を出す ---
59
- // .paths には全点が必要。間引かれていると gen_paths.py がエラーにする。
60
- const exe = path.join(POKER_INSTALL_DIR, 'poker_cui.EXE');
61
- if (!fssync.existsSync(exe))
62
- return fail('poker_cui が見つかりません: ' + exe,
63
- '環境変数 POKER_INSTALL_PATH を確認してください。');
64
-
65
- const summary = yamlFile + '.summary';
66
- const r1 = await run(exe, [yamlFile, '-p', '-t'], { cwd: TASKS_DIR });
67
- if (r1.code !== 0 || !fssync.existsSync(summary)) {
68
- return fail('poker_cui -p の実行に失敗しました(終了コード ' + r1.code + ')',
69
- (r1.err || r1.out || '').slice(-400));
70
- }
71
-
72
- // --- 2. spec を組み立てる ---
73
- const out = args.output || path.join(TASKS_DIR, 'poker.paths');
74
- const spec = {
75
- fcstd: args.fcstd.replace(/\\/g, '/'),
76
- out: out.replace(/\\/g, '/'),
77
- poker_summary: summary.replace(/\\/g, '/'),
78
- detectors_from_summary: true,
79
- deviation: args.deviation ?? 0.5,
80
- unit_scale: args.unit_scale ?? 0.1,
81
- poker_dir: POKER_INSTALL_DIR.replace(/\\/g, '/'),
82
- mu_energy: args.mu_energy ?? 1.25,
83
- };
84
- if (args.source_name) spec.source_name = args.source_name;
85
- if (args.buildup_exclude) spec.buildup_exclude = args.buildup_exclude;
86
- if (args.chunk) spec.chunk = args.chunk;
87
-
88
- // カスタム材料のビルドアップ等価材料を自動で解決する。
89
- // gen_paths.py は層の縮約先を決める際、単層ビルドアップデータの有無を
90
- // 材料名で照合する。Source_Dry のようなカスタム材料は標準材料に
91
- // 読み替える必要があり、従来は spec.equivalent に手で書いていた。
92
- // MaterialCatalog が lib_equivalent.dat と実データから解決できるので
93
- // ここで埋める(POKER 側も同じ表を見るので食い違わない)。
94
- try {
95
- const data = taskManager.data || {};
96
- const equiv = {};
97
- for (const z of (data.zone || [])) {
98
- const m = z.material;
99
- if (!m || m === 'VOID' || equiv[m]) continue;
100
- if (MaterialCatalog.isStandard && MaterialCatalog.isStandard(m)) continue;
101
- const e = MaterialCatalog.nearestBuildupEquivalent(m);
102
- const name = (e && typeof e === 'object') ? e.equivalent : e;
103
- if (name) equiv[m] = name;
104
- }
105
- if (Object.keys(equiv).length) {
106
- spec.equivalent = equiv;
107
- logger.info('ビルドアップ等価材料を自動設定しました', { equivalent: equiv });
108
- }
109
- } catch (e) {
110
- logger.warn('等価材料の自動解決に失敗(spec.equivalent なしで続行)',
111
- { error: e.message });
112
- }
113
-
114
- const specFile = path.join(TASKS_DIR, '.generate_paths_spec.json');
115
- await fs.writeFile(specFile, JSON.stringify(spec, null, 2), 'utf8');
116
-
117
- // --- 3. FreeCAD をヘッドレスで起動してトレース ---
118
- // GUI の MCP ブリッジ経由では、数万レイのトレースが GUI スレッドを
119
- // 占有してブリッジが壊れる。freecadcmd を使う。
120
- const script =
121
- 'import sys, json, traceback\n' +
122
- 'sys.path.insert(0, r"' + CAD_TOOLS_DIR + '")\n' +
123
- 'try:\n' +
124
- ' import gen_paths\n' +
125
- ' gen_paths.main(r"' + specFile + '")\n' +
126
- ' print("GEN_PATHS_OK")\n' +
127
- 'except BaseException:\n' +
128
- ' print("GEN_PATHS_ERROR")\n' +
129
- ' traceback.print_exc()\n';
130
-
131
- const scriptFile = path.join(TASKS_DIR, '.generate_paths.py');
132
- await fs.writeFile(scriptFile, script, 'utf8');
133
-
134
- const r2 = await run(FREECAD_CMD, [scriptFile], { cwd: TASKS_DIR });
135
- const log = (r2.out || '') + (r2.err || '');
136
-
137
- if (!log.includes('GEN_PATHS_OK') || !fssync.existsSync(out)) {
138
- // Python のトレースバックから要点を抜く
139
- const m = log.match(/(?:SystemExit|Error|Exception)[^\n]*(?:\n[^\n]*){0,3}/);
140
- return fail('経路の生成に失敗しました',
141
- m ? m[0].trim() : log.slice(-500));
142
- }
143
-
144
- // --- 4. 生成結果を要約する ---
145
- const text = await fs.readFile(out, 'utf8');
146
- const info = {};
147
- for (const key of ['n_sources', 'n_source_points', 'n_detectors', 'n_materials',
148
- 'format_version', 'deviation_mm']) {
149
- const m = text.match(new RegExp('^\\s*' + key + ':\\s*(.+)$', 'm'));
150
- if (m) info[key] = m[1].trim();
151
- }
152
- const nPaths = (text.split('paths: |')[1] || '').trim().split('\n').length;
153
-
154
- logger.info('経路ファイルを生成しました', { out, ...info });
155
-
156
- return {
157
- content: [{
158
- type: 'text',
159
- text: JSON.stringify({
160
- success: true,
161
- message: '経路ファイルを生成しました',
162
- output: out,
163
- summary: {
164
- 経路数: nPaths,
165
- 線源数: info.n_sources,
166
- 線源点数: info.n_source_points,
167
- 検出器評価点数: info.n_detectors,
168
- 材質数: info.n_materials,
169
- テッセレーション偏差_mm: info.deviation_mm,
170
- format_version: info.format_version
171
- },
172
- spec_used: {
173
- deviation: spec.deviation,
174
- unit_scale: spec.unit_scale,
175
- mu_energy: spec.mu_energy,
176
- buildup_exclude: spec.buildup_exclude ?? ['VOID', 'Air'],
177
- equivalent: spec.equivalent ?? {},
178
- freecad: FREECAD_CMD,
179
- poker_dir: spec.poker_dir
180
- },
181
- next: 'poker_cui "' + yamlFile + '" --path-input "' + out + '" -t で計算できます'
182
- }, null, 2)
183
- }]
184
- };
185
- }
186
- };
187
- }
1
+ // mcp/handlers/cadHandlers.js
2
+ //
3
+ // CAD 連携レイトレース。従来は次の 3 手順を手で行う必要があった。
4
+ // 1. spec.json を手書き(絶対パスを 4 箇所)
5
+ // 2. poker_cui -p で分割点と評価点を出す
6
+ // 3. freecadcmd を起動して gen_paths.py を走らせる
7
+ // このハンドラがまとめて行う。
8
+
9
+ import { spawn } from 'child_process';
10
+ import fs from 'fs/promises';
11
+ import fssync from 'fs';
12
+ import path from 'path';
13
+ import { FREECAD_CMD, CAD_TOOLS_DIR, POKER_INSTALL_DIR, TASKS_DIR, YAML_FILE }
14
+ from '../../utils/paths.js';
15
+ import { logger } from '../../utils/logger.js';
16
+ import { MaterialCatalog } from '../../utils/MaterialCatalog.js';
17
+
18
+ function run(cmd, args, opts = {}) {
19
+ return new Promise((resolve) => {
20
+ const p = spawn(cmd, args, { ...opts, stdio: ['ignore', 'pipe', 'pipe'] });
21
+ let out = '', err = '';
22
+ p.stdout.on('data', d => out += d);
23
+ p.stderr.on('data', d => err += d);
24
+ p.on('error', e => resolve({ code: -1, out, err: e.message }));
25
+ p.on('close', code => resolve({ code, out, err }));
26
+ });
27
+ }
28
+
29
+ export function createCadHandlers(taskManager) {
30
+ return {
31
+ async generatePaths(args) {
32
+ const fail = (msg, hint) => ({
33
+ content: [{
34
+ type: 'text',
35
+ text: JSON.stringify({ success: false, error: msg, hint }, null, 2)
36
+ }],
37
+ isError: true
38
+ });
39
+
40
+ // --- FreeCAD の所在 ---
41
+ if (!FREECAD_CMD) {
42
+ return fail(
43
+ 'FreeCAD が見つかりません',
44
+ '環境変数 FREECAD_PATH に freecadcmd の場所を設定してください。' +
45
+ '実行ファイル(C:/Program Files/FreeCAD 1.1/bin/freecadcmd.exe)でも、' +
46
+ 'インストールフォルダ(C:/Program Files/FreeCAD 1.1)でも指定できます。' +
47
+ 'claude_desktop_config.json の env に追加します。');
48
+ }
49
+ if (!args || !args.fcstd) return fail('fcstd を指定してください');
50
+ if (!fssync.existsSync(args.fcstd))
51
+ return fail('CAD ファイルが見つかりません: ' + args.fcstd);
52
+
53
+ const yamlFile = YAML_FILE;
54
+ if (!fssync.existsSync(yamlFile))
55
+ return fail('入力 YAML がありません: ' + yamlFile,
56
+ 'applyChanges で入力を確定してから実行してください。');
57
+
58
+ // --- 1. poker_cui -p で分割点と評価点を出す ---
59
+ // .paths には全点が必要。間引かれていると gen_paths.py がエラーにする。
60
+ const exe = path.join(POKER_INSTALL_DIR, 'poker_cui.EXE');
61
+ if (!fssync.existsSync(exe))
62
+ return fail('poker_cui が見つかりません: ' + exe,
63
+ '環境変数 POKER_INSTALL_PATH を確認してください。');
64
+
65
+ // 専用のサマリーに出す。
66
+ // 既定の <yaml>.summary を使うと executeCalculation と衝突する。
67
+ // あちらは -p なしで同じファイルを上書きするので、次に generatePaths を
68
+ // 呼んだとき input: セクションが無く「point_source が見つかりません」で
69
+ // 失敗する。用途が違うファイルは分ける。
70
+ const summary = path.join(TASKS_DIR, '.generate_paths.summary');
71
+ const r1 = await run(exe, [yamlFile, '-p', '-t', '-o', summary,
72
+ '-d', path.join(TASKS_DIR, '.generate_paths.dose')],
73
+ { cwd: TASKS_DIR });
74
+ if (r1.code !== 0 || !fssync.existsSync(summary)) {
75
+ return fail('poker_cui -p の実行に失敗しました(終了コード ' + r1.code + ')',
76
+ (r1.err || r1.out || '').slice(-400));
77
+ }
78
+
79
+ // --- 2. spec を組み立てる ---
80
+ const out = args.output || path.join(TASKS_DIR, 'poker.paths');
81
+ const spec = {
82
+ fcstd: args.fcstd.replace(/\\/g, '/'),
83
+ out: out.replace(/\\/g, '/'),
84
+ poker_summary: summary.replace(/\\/g, '/'),
85
+ detectors_from_summary: true,
86
+ deviation: args.deviation ?? 0.5,
87
+ unit_scale: args.unit_scale ?? 0.1,
88
+ poker_dir: POKER_INSTALL_DIR.replace(/\\/g, '/'),
89
+ mu_energy: args.mu_energy ?? 1.25,
90
+ };
91
+ if (args.source_name) spec.source_name = args.source_name;
92
+ if (args.buildup_exclude) spec.buildup_exclude = args.buildup_exclude;
93
+ if (args.chunk) spec.chunk = args.chunk;
94
+
95
+ // カスタム材料のビルドアップ等価材料を自動で解決する。
96
+ // gen_paths.py は層の縮約先を決める際、単層ビルドアップデータの有無を
97
+ // 材料名で照合する。Source_Dry のようなカスタム材料は標準材料に
98
+ // 読み替える必要があり、従来は spec.equivalent に手で書いていた。
99
+ // MaterialCatalog が lib_equivalent.dat と実データから解決できるので
100
+ // ここで埋める(POKER 側も同じ表を見るので食い違わない)。
101
+ try {
102
+ const data = taskManager.data || {};
103
+ const equiv = {};
104
+ for (const z of (data.zone || [])) {
105
+ const m = z.material;
106
+ if (!m || m === 'VOID' || equiv[m]) continue;
107
+ if (MaterialCatalog.isStandard && MaterialCatalog.isStandard(m)) continue;
108
+ const e = MaterialCatalog.nearestBuildupEquivalent(m);
109
+ const name = (e && typeof e === 'object') ? e.equivalent : e;
110
+ if (name) equiv[m] = name;
111
+ }
112
+ if (Object.keys(equiv).length) {
113
+ spec.equivalent = equiv;
114
+ logger.info('ビルドアップ等価材料を自動設定しました', { equivalent: equiv });
115
+ }
116
+ } catch (e) {
117
+ logger.warn('等価材料の自動解決に失敗(spec.equivalent なしで続行)',
118
+ { error: e.message });
119
+ }
120
+
121
+ const specFile = path.join(TASKS_DIR, '.generate_paths_spec.json');
122
+ await fs.writeFile(specFile, JSON.stringify(spec, null, 2), 'utf8');
123
+
124
+ // --- 3. FreeCAD をヘッドレスで起動してトレース ---
125
+ // GUI の MCP ブリッジ経由では、数万レイのトレースが GUI スレッドを
126
+ // 占有してブリッジが壊れる。freecadcmd を使う。
127
+ const script =
128
+ 'import sys, json, traceback\n' +
129
+ 'sys.path.insert(0, r"' + CAD_TOOLS_DIR + '")\n' +
130
+ 'try:\n' +
131
+ ' import gen_paths\n' +
132
+ ' gen_paths.main(r"' + specFile + '")\n' +
133
+ ' print("GEN_PATHS_OK")\n' +
134
+ 'except BaseException:\n' +
135
+ ' print("GEN_PATHS_ERROR")\n' +
136
+ ' traceback.print_exc()\n';
137
+
138
+ const scriptFile = path.join(TASKS_DIR, '.generate_paths.py');
139
+ await fs.writeFile(scriptFile, script, 'utf8');
140
+
141
+ const r2 = await run(FREECAD_CMD, [scriptFile], { cwd: TASKS_DIR });
142
+ const log = (r2.out || '') + (r2.err || '');
143
+
144
+ if (!log.includes('GEN_PATHS_OK') || !fssync.existsSync(out)) {
145
+ // Python のトレースバックから要点を抜く
146
+ const m = log.match(/(?:SystemExit|Error|Exception)[^\n]*(?:\n[^\n]*){0,3}/);
147
+ return fail('経路の生成に失敗しました',
148
+ m ? m[0].trim() : log.slice(-500));
149
+ }
150
+
151
+ // --- 4. 生成結果を要約する ---
152
+ const text = await fs.readFile(out, 'utf8');
153
+ const info = {};
154
+ for (const key of ['n_sources', 'n_source_points', 'n_detectors', 'n_materials',
155
+ 'format_version', 'deviation_mm']) {
156
+ const m = text.match(new RegExp('^\\s*' + key + ':\\s*(.+)$', 'm'));
157
+ if (m) info[key] = m[1].trim();
158
+ }
159
+ const nPaths = (text.split('paths: |')[1] || '').trim().split('\n').length;
160
+
161
+ logger.info('経路ファイルを生成しました', { out, ...info });
162
+
163
+ return {
164
+ content: [{
165
+ type: 'text',
166
+ text: JSON.stringify({
167
+ success: true,
168
+ message: '経路ファイルを生成しました',
169
+ output: out,
170
+ summary: {
171
+ 経路数: nPaths,
172
+ 線源数: info.n_sources,
173
+ 線源点数: info.n_source_points,
174
+ 検出器評価点数: info.n_detectors,
175
+ 材質数: info.n_materials,
176
+ テッセレーション偏差_mm: info.deviation_mm,
177
+ format_version: info.format_version
178
+ },
179
+ spec_used: {
180
+ deviation: spec.deviation,
181
+ unit_scale: spec.unit_scale,
182
+ mu_energy: spec.mu_energy,
183
+ buildup_exclude: spec.buildup_exclude ?? ['VOID', 'Air'],
184
+ equivalent: spec.equivalent ?? {},
185
+ freecad: FREECAD_CMD,
186
+ poker_dir: spec.poker_dir
187
+ },
188
+ next: 'poker_cui "' + yamlFile + '" --path-input "' + out + '" -t で計算できます'
189
+ }, null, 2)
190
+ }]
191
+ };
192
+ }
193
+ };
194
+ }
@@ -1,7 +1,7 @@
1
1
  // services/CalculationService.js
2
2
  import { spawn } from 'child_process';
3
3
  import { promises as fs } from 'fs';
4
- import path from 'path';
4
+ import path from 'path';
5
5
  import { TASKS_DIR } from '../utils/paths.js';
6
6
  import { logger } from '../utils/logger.js';
7
7
  import { CalculationError } from '../utils/errors.js';