Skip to main content
Glama
ymzkd

FEMNet MCP Apps Server

by ymzkd
README.md
# FEMNet MCP Apps サーバー

FEMNet(C++ 製 FEM ライブラリの Python バインディング)を解析エンジンとして、
**Claude Desktop のチャット画面内に vtk.js の 3D 構造ビューアを開く** MCP Apps サーバー。

Agent はモデル定義・解析実行・表示切り替えをツール呼び出しだけで行う。
描画コードを毎回生成させないので、トークン消費と生成エラーを避けられる。

```
Claude Desktop ──stdio── server.py ── fem_core.py ── femnet (.pyd)
       │                     │
       │ ui://fem/app.html   └── fem_view.py ──┐ 表示メッシュを
       │  (iframe)                             │ バイナリで配信
       └──────── fetch http://127.0.0.1:8781/view ┘
```

メッシュは数 MB になるため MCP の JSON-RPC には載せず、
UI(iframe)からローカル HTTP へ直接 fetch させる。
HTTP が塞がれた場合は `fem_view_chunk` ツールで MCP チャネル経由の分割取得にフォールバックする。

## 必要環境

| 項目 | 条件 |
| --- | --- |
| OS | Windows(FEMNet のビルド成果物が Windows 向け) |
| Python | **3.12 固定**(`_femnet.pyd` が `python312.dll` にリンク済み) |
| パッケージ | `mcp>=2.0.0`, `numpy>=2.0`(`pydantic` は `mcp` 経由) |
| 外部依存 | FEMNet のビルド済み `_femnet.pyd`、Intel oneAPI MKL |
| ホスト | Claude Desktop(ローカル stdio 接続) |

`.venv` は `uv` が管理する。`uv.lock` をコミット済み。

## セットアップ

### 1. 依存の取得

```bash
uv sync
```

### 2. FEMNet の場所を教える

FEMNet のビルド成果物はこのリポジトリの外にあり、置き場所はマシンごとに違う。
そのためパスはコードに書かず、`femnet_env.py` が次の順で解決する。

1. 環境変数 `FEMNET_PYTHON_DIR`
2. `femnet_local.py`(リポジトリ管理外)に書いた `FEMNET_PYTHON_DIR`

どちらも無ければ起動時に明示的なエラーになる。手軽なのは 2 の方法。

```python
# femnet_local.py(.gitignore 済み。自分の環境のパスを書く)
FEMNET_PYTHON_DIR = r"C:\path\to\StructureMM\lib\FEMNet\python"
```

指定するのは `femnet/_femnet.pyd` を含む `python/` ディレクトリ。

`.pyd` は MKL の DLL に依存する。Python 3.8 以降は `PATH` を DLL 検索に使わないため、
`os.add_dll_directory()` での明示登録が必須(`femnet_env.setup()` が行う)。
oneAPI の既定パスを見にいくので通常は設定不要だが、別の場所に入れている場合は
環境変数 `FEMNET_MKL_DLL_DIRS`(`;` 区切り)で上書きできる。

### 3. Claude Desktop に登録

`claude_desktop_config.json`:

```json
{
  "mcpServers": {
    "femnet": {
      "command": "C:\\Users\\<user>\\.local\\bin\\uv.exe",
      "args": ["run", "--directory", "C:\\path\\to\\MCPAppTest", "server.py"],
      "env": { "FEMNET_PYTHON_DIR": "C:\\path\\to\\FEMNet\\python" }
    }
  }
}
```

`command` は絶対パスにする(GUI 起動の Claude Desktop は PATH 解決が不安定)。
設定変更後はトレイから完全終了して再起動する。

## ツール

| ツール | UI | 役割 |
| --- | :-: | --- |
| `build_model` | ● | 節点・材料・断面・要素・支持・荷重を JSON で一括定義し、形状を表示 |
| `run_static` | ● | 線形静解析 → 変形図+変位コンター |
| `show_view` | ● | 解析済み結果の表示切り替え(変形図 / 板応力コンター / 部材断面力図) |
| `run_modal` | ● | 固有値解析 → モード形状アニメーション |
| `get_results` | | 節点変位・反力・部材応力を数値で返す(Agent の考察用) |
| `fem_view_chunk` | | UI 専用。表示メッシュを base64 で分割取得するフォールバック |

`show_view` の指定:

- `view="deformation"` … `component` = `mag` / `ux` / `uy` / `uz`
- `view="beam_diagram"` … `component` = `N` / `Qy` / `Qz` / `Mx` / `My` / `Mz`(M 図なら `Mz`、Q 図なら `Qy`)
- `view="plate_stress"` … `component` = `Mx` / `My` / `Mxy` / `Qx` / `Qy` / `Nx` / `Ny` / `Qxy`

LLM が書きがちな別名(`moment`, `曲げ`, `axial` など)は `server.py` 側で正規名に吸収している。

## モデル定義

単位系は **N-mm 系**。

| 量 | 単位 | 例(鋼) |
| --- | --- | --- |
| 長さ・座標 | mm | |
| 力 | N | |
| モーメント | N·mm | |
| ヤング係数 `e` | N/mm² | 205000 |
| 密度 `density` | **kg/m³** | 7850 |
| 節点付加質量 `mass_kg` | kg | |
| 分布荷重 `w0` / `w1` | N/mm | |

要素タイプ(`ElementSpec.type`):

| type | 節点数 | 必須 |
| --- | :-: | --- |
| `beam` | 2 | `section` |
| `truss` | 2 | `section` |
| `tri_plate` | 3 | `thickness` |
| `quad_plate` | 4 | `thickness` |

支持は `preset="fixed"` / `"pinned"` か、`ux`…`rz` の個別フラグ(True = 拘束)。
固有値解析には質量が要る(材料の `density` か `node_masses`)。

スキーマの全体は [fem_core.py](fem_core.py) の `ModelSpec` を参照。

## ファイル構成

| ファイル | 役割 |
| --- | --- |
| [server.py](server.py) | MCP サーバー本体。ツール定義、UI リソース登録、起動 |
| [fem_core.py](fem_core.py) | モデル定義スキーマ(pydantic)と FEMNet 呼び出し・解析実行 |
| [fem_view.py](fem_view.py) | 解析結果 → 表示メッシュ変換、バイナリ化、ローカル HTTP 配信 |
| [femnet_env.py](femnet_env.py) | `_femnet.pyd` と MKL DLL の検索パス解決 |
| `femnet_local.py` | 環境固有のパス設定。リポジトリには含めない(`.gitignore` 済み) |
| [ui/fem.html](ui/fem.html) | ビューア UI。起動時に JS バンドルを差し込んで単一リソースとして配信 |
| [ui/vtk.js](ui/vtk.js) | vtk.js のツリーシェイク済みバンドル(693 KB) |
| [ui/ext-apps.js](ui/ext-apps.js) | `@modelcontextprotocol/ext-apps` のバンドル(383 KB) |
| [mesh.py](mesh.py), [ui/mesh.html](ui/mesh.html), [ui/app.html](ui/app.html) | 経路検証に使った初期プロトタイプ。現在のサーバーからは参照していない |

`ui/*.js` はビルド成果物だがリポジトリに含める。これによりサーバー実行時は
Python / uv だけで完結し、Node は**バンドル生成時のみ**必要になる。

```bash
npm install --no-save @kitware/vtk.js
npx esbuild entry.js --bundle --format=iife --global-name=VTK --minify \
  --define:process.env.NODE_ENV='"production"' --loader:.glsl=text --outfile=ui/vtk.js
```

## 実装上の制約メモ

実機でしか判明しなかった、変更時に踏み抜きやすい点。

- **`structuredContent` は UI に届かない。** Claude Desktop は `ui/notifications/tool-result` で
  `structuredContent` を落とし、`content` の text(JSON 文字列)だけを渡す。UI 側は必ず両方を見る。
- **ローカル HTTP への fetch は CORS だけでは通らない。** iframe は公開オリジン(https)で動くため
  `http://127.0.0.1` への要求は Private Network Access の対象になる。
  `Access-Control-Allow-Private-Network: true` と OPTIONS への応答が要る。
- **CSP の宣言が必須で、ポート確定との順序制約がある。** localhost も `_meta.ui.csp` の
  `connect_domains` に書く。ポート番号を含むので、HTTP サーバーを起動して
  ポートを確定させてから `add_html_resource(csp=...)` を呼ぶ。
- **バイナリのヘッダ長は 4 バイト境界に揃える。** `new Float32Array(buffer, offset, ...)` は
  offset が 4 の倍数でないと `RangeError` になる。JSON ヘッダは空白でパディングする。
- **`<script src="...">` は使えない。** iframe の中には取得元サーバーが無い。
  `load_ui()` が HTML 内のプレースホルダにバンドルを差し込む。
- **支持条件は要素生成より前に適用する。** このビルドの FEMNet は要素生成後の `Fix` 代入で
  板要素の解が壊れる(単純支持板のたわみが 1/10 になるのを実測)。
- **`MassData.Mass` は質量ではなく重量 [N]。** 材料 `density` も内部では単位体積重量 [N/mm³]
  として扱われる(質量行列で `g` で割られる)。`fem_core.py` が kg / kg·m⁻³ から換算している。
- **取得経路は二重化してある。** UI は `ontoolresult`(push)で `data_url` を受け取り、
  ローカル HTTP → 失敗時は `fem_view_chunk` の MCP 分割取得、の順に落とす。
  CSP 違反は `securitypolicyviolation` で拾って画面に出す。

## 表示メッシュのバイナリ形式(FEM4)

```
"FEM4" | uint32 ヘッダ長 | JSON ヘッダ(4 バイト境界) |
verts f32×3V | tris u32×3T |
fields × (f32×3V)              … 変位ベクトル場(静解析=1本、モード=モード数)
plate_components × (f32×T)     … 三角形ごとの板応力(板由来でなければ NaN)
beam_values f32 × (B × components × stations)   … 部材断面力ダイアグラム用
```

スカラー(`|u|` や成分)は UI 側でベクトルから計算する。
変形倍率の変更は非変形座標と変位ベクトルを別に持つことで、再取得なしに反映される。