# 自動操作のための入口 / Automation & agent guide

このアプリには**サーバがありません**。配信されるのは静的ファイルだけで、
計算はブラウザの中で走ります。したがって機械向けの入口も、すべて
**ページの中**にあります。

1. **`window.Optics` / `window.SpectraData`** — 画面を介さず計算だけ呼ぶ
2. **`window.__opd`** — 画面を操作するエージェントが、クリックやドラッグの
   代わりに使う入口
3. **`data-*` 属性** — 画面の状態を読む

どれも読み取りと計算だけで、どこにも書き込みません。

> 以前の版には `/api/...` の HTTP API がありました。サーバ依存を無くすため
> 廃止し、同じ形の答えを `SpectraData`（`static/data.js`）が返します。
> 外のプロセスから叩きたい場合は、下の「MCP サーバにする場合」を見てください。

---

## 1. ページ内の計算 API

`design.html` / `spectra.html` を開くと、次の2つが生えます。

| | |
|---|---|
| `window.Optics` | 光路の計算そのもの（`static/optics.js`） |
| `window.SpectraData` | スペクトルの取り出しと、旧 API と同じ形の答え（`static/data.js`） |

```js
// 部品を探す → スペクトルを取る → 光路を解析する
const rows = (await SpectraData.search({category: "F", subtype: "BP"})).rows;
const curve = await SpectraData.curve(rows[0].id);   // [[nm, 透過率], ...]
const res = await SpectraData.analyze({nodes, edges});
```

`SpectraData` の一覧:

| 呼び出し | 返り値 |
|---|---|
| `stats()` | 収録件数と出典 |
| `search({category, subtype, q, limit})` | `{rows: [{id, category, subtype, owner_name, maker}]}` |
| `curve(id)` | `[[nm, 値], ...]`（300–1000nm を 1nm 刻み、701 点） |
| `dyes(limit)` | `{rows: [{slug, name, absorption, emission}]}` |
| `analyze(body)` | 下記（旧 `POST /api/design/analyze` と同じ形） |
| `channels(body)` | チャンネル別の励起効率・収集効率・漏れ込み |
| `csv(body)` | 波長を行にした CSV（`Blob`） |

`category` は `F`（フィルタ類）/ `L`（光源）/ `C`（カメラ・検出器）。
`subtype` は `BP` バンドパス / `LP` ロングパス / `SP` ショートパス /
`BS` ダイクロ / `EX` 励起 / `EM` 蛍光 / `BX` 励起ブロック / `BM` 蛍光ブロック /
`NF` ノッチ / `ND` ND。

`Optics` は素の計算で、スペクトルの取得を伴いません
（`analyze` / `validate` / `channel` / `crosstalk` / `toGrid` / `integrate` /
`multiply` / `invert` / `summarize`）。曲線を自分で用意できるなら、
`Optics.analyze(nodes, edges)` だけでデータ無しに計算できます。

### `analyze` に渡す形

```jsonc
{
  "nodes": [
    {"id": "L", "type": "light",    "name": "光源",     "spectrum": 123, "power": 1},
    {"id": "X", "type": "filter",   "name": "励起F",    "spectrum": 456},
    {"id": "D", "type": "dichroic", "name": "ダイクロ", "spectrum": 789},
    {"id": "S", "type": "sample",   "name": "試料", "scatter": 0.001,
     "fluorophores": [{"name": "EGFP", "brightness": 1,
                       "absorption": 111, "emission": 222}]},
    {"id": "M", "type": "filter",   "name": "蛍光F",    "spectrum": 654},
    {"id": "C", "type": "detector", "name": "検出器"}
  ],
  "edges": [
    {"src": "L", "port": "out",      "dst": "X"},
    {"src": "X", "port": "out",      "dst": "D"},
    {"src": "D", "port": "reflect",  "dst": "S"},
    {"src": "S", "port": "out",      "dst": "D"},
    {"src": "D", "port": "transmit", "dst": "M"},
    {"src": "M", "port": "out",      "dst": "C"}
  ]
}
```

* `type`: `light` / `filter` / `mirror` / `dichroic` / `sample` / `detector` /
  `probe`（光を遮らない仮想の検出器）
* `port`: `out`（1 出力の部品）、ダイクロだけ `transmit` と `reflect`
* `spectrum` は `SpectraData.search()` が返す数値 id。手元のデータを使う場合は
  代わりに `spectrum_curve: [[nm, 透過率0-1], ...]` を同梱できます
  （`absorption_curve` / `emission_curve` も同様）。どこにも保存されません。

返り値は検出器ごとに、届いた総量 `total`、光源別の内訳 `by_source`、比率
`ratio`、主成分 `main`、S/漏れ `signal_to_bleed`、励起光の漏れ
`excitation_leak` を返します。`warnings` につなぎ忘れの指摘が入ります。

Node から動かすこともできます（画面もサーバも要りません）。

```js
const Optics = require("./static/optics.js");
const res = Optics.analyze(nodes, edges);   // 曲線は自分で入れる
console.log(res.detectors);
```

---

## 2. `window.__opd`（画面を操作するエージェント向け）

`design.html` を開くと `window.__opd` が生えます。ドラッグや
`<select>` のクリックをしなくても、配置・部品選び・計算ができます。
ページ内で `__opd.help()` を呼べば、この節の要約が返ります。

```js
await window.__opd.waitReady();     // 起動（部品一覧の読み込み）を待つ

__opd.clear();
__opd.addBlock("light",    0, 2, {dir: 0, name: "励起光源"});
__opd.addBlock("cube",     3, 2, {orient: "\\", ex: "FF01-469/35",
                                  di: "MD498", em: "FF01-525/39"});
__opd.addBlock("sample",   3, 5, {dyes: ["EGFP"]});
__opd.addBlock("detector", 3, 0);

const runs = await __opd.run();
runs[0].res.detectors;              // {id: {name, total, by_source, ratio, …}}
```

### 一覧

| 呼び出し | 返り値 |
|---|---|
| `waitReady()` | 起動完了を待つ Promise |
| `help()` | 使い方（文字列） |
| `getState()` | `{ready, view, lang, state, blocks, nodes, edges, dbParts, dyes, error, warning, hasResult}` |
| `getDesign()` / `setDesign(d)` | 設計まるごと（「設定を保存」の JSON と同じ形） |
| `setView("layout" \| "node")` | 実装図／ノード図の切替 |
| `listKinds()` | 置ける部品の種類 |
| `findParts(kind, q, limit)` | `[{id, name, maker, subtype, mine}]` |
| `findDyes(q, limit)` | `[{slug, name}]` |
| `getBlocks()` | 実装図の全ブロック |
| `addBlock(kind, col, row, props)` | 置いたブロック |
| `setBlock(id, props)` | 更新後のブロック |
| `moveBlock(id, col, row)` / `removeBlock(id)` | — |
| `clear()` / `preset("cube" \| "epi1" \| "epi2")` | 状態 |
| `run()` | `[{label, res}]`（await する） |
| `getResult()` | 直近の `run()` と同じもの |

### `setBlock` / `addBlock` の props

| キー | 対象 | 値 |
|---|---|---|
| `name`, `show` | 全部 | 表示名、スペクトルを図に出すか |
| `power` | 光源 | 相対強度 |
| `reflectance` | ミラー | 0–1 |
| `scatter` | サンプル | 戻る励起光の割合 |
| `dir` | 光源 | `0`=→ `1`=↓ `2`=← `3`=↑ |
| `orient` | ダイクロ・キューブ・ミラー | `"\\"` または `"/"` |
| `pdir` | 仮想検出器 | `0`–`3`、`null` で両方向 |
| `spectrum` | フィルタ・ダイクロ単体・検出器 | 部品 |
| `ex`, `di`, `em` | キューブ | 励起F・ダイクロ・蛍光F |
| `dyes` | サンプル | `["EGFP", {name: "mCherry", brightness: 0.5}]`（最大 3） |

部品と色素は **数値 id でも型番・名前の文字列でも**指定できます。
`"MD498"` のように一意に決まればそれを選び、決まらなければ**黙って選ばずに
候補を並べた例外**を投げます。

```js
__opd.setBlock(id, {di: "M"});
// Error: 「M」に当てはまる部品が 1142 件あります。型番まで指定してください: …
```

### DOM から読む場合

| 場所 | 属性 |
|---|---|
| `<body>` | `data-state` = `loading` / `ready` / `busy` / `error`、`data-view`、`data-lang` |
| 実装図のブロック `.blk` | `data-id` `data-kind` `data-name` `data-col` `data-row` `data-selected` `data-spectrum` `data-ex` `data-di` `data-em` `data-dir` `data-orient` `data-pdir` |
| ノード図の部品 `.node` | `data-id` `data-type` `data-name` `data-spectrum` |

計算の完了は `body[data-state]` が `busy` → `ready`（失敗なら `error`）に
変わることで分かります。エラー本文は `#error`、注意書きは `#warn` に出ます。

### 注意

* 部品を選ばないフィルタ・ダイクロは「素通し（透過 100%）」として扱われます。
  そのため未選択のキューブでは励起光が試料へ曲がらず、結果が全部 0 になります。
  この場合は画面に警告が出ますが、`getState().warning` でも読めます。
* `run()` は `#error` に何か出たら例外を投げます。
* 保存は `localStorage`（キー `thorparts.design.v1`）です。まっさらな状態から
  始めたいときは新しいブラウザコンテキストを使ってください。

---

## 3. WebMCP（対応ブラウザでは、これが一番素直）

[WebMCP](https://webmachinelearning.github.io/webmcp/) に対応したブラウザ
（Chrome 146 で試験提供）では、`document.modelContext` にこの画面の操作が
**道具として登録済み**です。`__opd` の使い方を知らなくても、エージェントが
道具を見つけて呼べます。

| 道具 | すること |
|---|---|
| `search_parts` | フィルタ・ダイクロ・光源・検出器を探す |
| `search_dyes` | 色素・蛍光タンパクを探す |
| `build_epi_path` | 落射蛍光の光路を**1回で**組む |
| `analyze_light_path` | 計算して、検出器ごとの内訳を返す |
| `get_light_path` / `set_light_path` | 設計の取り出しと復元 |
| `load_fpbase_microscope` | FPbase の顕微鏡構成から組む |

粒度は「エージェントがやりたいこと」に合わせてあります。`addBlock` を
そのまま出すと往復が増えるので、`build_epi_path` のように1回で終わる形に
しました。細かく触りたいときは `__opd` をそのまま使えます。

読むだけの道具には `readOnlyHint`、FPbase 由来の文字列を返す道具には
`untrustedContentHint` を付けてあります。

**外部スクリプトは読み込みません。**登録はこのページの中で完結するので、
CSP は「外部への通信を許さない」ままです。

```js
// エージェント側から見た使い方
const tools = await document.modelContext.getTools();
await document.modelContext.executeTool("build_epi_path",
  JSON.stringify({dichroic: "MD498", excitation: "FF01-469/35",
                  emission: "FF01-525/39", dye: "EGFP"}));
await document.modelContext.executeTool("analyze_light_path", "{}");
```

### まだ対応していないブラウザでは

`document.modelContext` は**ブラウザが用意するもの**で、ページ側では作れません
（`navigator.modelContext` は仕様から外れた古い綴りです）。無い場合でも同じ道具
を使えるよう、次の2つをいつでも出してあります。JS を評価できるエージェント
（Claude in Chrome など）はこれで足ります。

```js
window.__opdToolList().map(t => t.name);            // 道具の一覧（名前・説明・入力の形）
await window.__opdCall("build_epi_path", {dichroic: "MD498", dye: "EGFP"});
await window.__opdCall("analyze_light_path", {});
```

`__opdCall` は `document.modelContext.executeTool` と同じものを呼びます
（引数はオブジェクトでも JSON 文字列でも可、返り値は文字列）。道具が出そろうと
`<body>` に `data-opd-tools="7"` が付きます。`registerTool` まで済んだときだけ
`data-webmcp` が付くので、どちらの経路かは属性で見分けられます。

## 3.5 URL で光路を渡す（JS を実行できない相手向け）

`/design` のハッシュに書けば、開いた時点で光路が組み上がり、計算まで
終わります。**クリックと URL 移動しかできないエージェント**でも操作できます。

```
/design#epi=di:MD498;ex:FF01-469/35;em:FF01-525/39;dye:EGFP
```

`名前:値` を `;` で並べます。値に `;` や `%` が入るときは URL エンコード
してください。使える名前は `build_epi_path` と同じで、短くも書けます。

| 名前 | 別名 | 何を置くか |
|---|---|---|
| `di` | `dichroic` | ダイクロ（**必須**） |
| `ex` | `excitation` | 励起フィルタ |
| `em` | `emission` | 蛍光フィルタ |
| `dye` | `dyes` | 試料の色素 |
| `light` | `source` | 光源（`laser:488` も可） |
| `det` | `detector` | 検出器の量子効率 |

もう一つ、設計まるごとを載せる形もあります。画面の「リンクを作る」が
出すのがこれで、共有や再現に使います。

```
/design#z=<設計JSONを deflate で縮めて base64url にしたもの>
/design#d=<設計JSONを base64url にしたもの>（圧縮なしの旧形式。今も開ける）
```

`#z=` が今の形式です。deflate は可逆圧縮なので設計は1ビットも変わらず、
URL は `#d=` より6割ほど短くなります。読む側はどちらも受けます。

ハッシュはサーバへ送信されないので、どちらの形でも設計は外に出ません。
読めない指定は画面の `#error` に理由が出ます。

---

## 4. MCP（Streamable HTTP）— ブラウザを介さない入口

**これが一番確実です。**相手がどのブラウザを使っているか、そもそも
ブラウザなのかに依存しません。エンドポイントは公開済みで、何も
インストールせずに URL 1行で使えます。

```bash
codex mcp add optical-path --url https://optical-path-designer.fpbox.workers.dev/mcp
```

```bash
claude mcp add --transport http optical-path https://optical-path-designer.fpbox.workers.dev/mcp
```

| 道具 | すること |
|---|---|
| `search_parts` | フィルタ・ダイクロ・光源・検出器を型番や語で探す |
| `search_dyes` | 色素・蛍光タンパクを探す |
| `design_epi_path` | 落射蛍光を組んで計算する（**1回で済む**） |
| `analyze_light_path` | 任意の光路を計算する（保存した設計ファイルもそのまま渡せる） |
| `get_spectrum` | 曲線を `[[nm, 値], …]` で取り出す |
| `list_microscopes` | FPbase の顕微鏡と、その光路構成を見る |

すべて読むだけ（`readOnlyHint`）で、どこにも書き込みません。認証もセッションも
無く、`initialize` → `tools/call` をそのまま POST できます。

### 画面が無いので状態を持ちません

「今の光路」という概念がありません。`design_epi_path` は組み立てた設計と
計算結果をその場で返します。続きを人が見たいときのために、同じ光路を開く
URL（`open_in_browser`）も返します。

```jsonc
// design_epi_path {"dichroic":"MD498","excitation":"FF01-469/35",
//                  "emission":"FF01-525/39","dye":"EGFP"} の返り値
{
  "detectors": {"C": {"total": 15.368, "by_source": {"EGFP": 15.368,
                      "励起光": 4.2e-9}, "main": "EGFP", …}},
  "warnings": [],
  "open_in_browser": "https://…/design#epi=di:MD498;ex:FF01-469%2F35;…",
  "design": {"nodes": […], "edges": […]}     // analyze_light_path に渡せる
}
```

計算は画面と**同じ `static/optics.js`** が Worker の中で走ります。同じ光路
なら答えは一致します（`tests/e2e/mcp.test.js` が数値で確認しています）。

### 画面で組んだ設計をそのまま渡す

「設定を保存」で書き出した JSON を、中身を組み直さずに `design` へ入れられます
（オブジェクトでも JSON 文字列でも可）。手で並べた光路を、そのまま計算に
かけられます。

```jsonc
// analyze_light_path
{"design": "{\"app\":\"thorparts-design\",\"version\":2,\"nodes\":[…],\"edges\":[…]}"}
```

**実装図（ブロックの並べ方）はそのまま持ち帰れます。**保存ファイルに
`layout` が入っていれば、返るリンクもそれを保ったままなので、開くと
自分の配置のまま実装図が出ます。逆は作れません――キューブ1個が3ノードに
開くので、ノード図から元の並べ方はたどれません。したがって、MCP だけで
一から組んだ光路（`analyze_light_path` に nodes / edges を直接渡した場合）は
**ノード図で開きます**。`design_epi_path` の `open_in_browser`（`#epi=`）は
画面側が組み立てるので実装図になります。

返り値には `open_in_browser` が付きます。**epi でない光路**――光源を合流
させる、検出器が複数、仮想検出器を挟む――でもリンクが出るので、計算結果を
画面で開いて確認したり、そこから手で詰めたりできます。`#epi=` では表せない
ので、設計そのものを `#z=`（deflate + base64url）に載せています。設計が大きすぎて
URL に載らない場合は `null` になり、理由が `open_in_browser_note` に入ります。

リンクはノード図で開きます。そこから**実装図へ起こすのはボタン1つ**です
（「ノード図から実装図へ」／`await __opd.buildBench()`）。落射蛍光と、その
手前で光源をまとめる形（合波）は、配置まで自動で組み上がります。光源が
増えれば合波が直列に増えるだけなので、**灯数に上限はありません**（マス目
12×7 に収まるかぎり）。

組めたかどうかは自称しません。**作った配置から接続を作り直して、元の光路と
同じ数字になるかを確かめます。**違えば捨てて、部品を置くだけに落とし
（`built: false`）、並べ替えは人にお願いします。作れたつもりで違う光路を
出すより、作れなかったと言うほうがよいからです。

### ダイクロは行きと帰りで別のノードにする

`analyze_light_path` に自分で `nodes` / `edges` を渡すときの注意です。
ダイクロを1つのノードで共有し、`reflect` を試料へ、試料の出力を同じ
ノードへ戻すと、**輪ができて信号が 12% 多く出ます**（帰りの蛍光が
ダイクロで反射して試料へ戻り、また出てくるため）。実物のキューブは
同じ膜を2回通るだけなので、通過ごとにノードを分けてください。

```jsonc
// 行き: L →(励起F)→ D1 --reflect--> S     D1 の transmit は繋がない
// 帰り: S → D2 --transmit--> (蛍光F) → C   D2 の reflect は繋がない
```

**光源をまとめるダイクロも同じです。**1つのノードに2灯を入れて両方を
`transmit` で出すと、実物では作れない光路になります（片方は反射で入る）。
通る向きごとに分けてください。

```jsonc
// L1 →→ MGt --transmit--> D1        MGt と MGr は同じ部品（同じ spectrum）
// L2 ↓↓ MGr --reflect---> D1
```

3灯以上も同じ書き方を重ねるだけです。**透過側が横軸の続き、反射側が上から
入る列**になります。

```jsonc
// L1 →→ At --transmit--> Bt --transmit--> D1
// L2 ↓↓ Ar --reflect---> Bt
// L3 ↓↓ Br --reflect---> D1
```

この形で書いておくと、実装図も自動で起こせます。

### 自分で立てる場合

`mcp.js` は Worker 用ですが、中身は静的ファイルを読んで `optics.js` を
呼ぶだけです。`dist/` を手元に置けば、同じものを stdio の MCP サーバとして
包み直すこともできます。

---

## 5. 動作確認

```bash
bash tests/e2e/run.sh agent      # window.__opd
bash tests/e2e/run.sh webmcp     # document.modelContext
```

`tests/e2e/agent.test.js` が、マウスを使わずに `window.__opd` と `data-*`
だけで光路を組んで計算するところまでを通しで確認します。
