← ブログに戻る

YAML→PNG を LLM に任せる — historymap に MCP サーバーを足した記録

クライアントに見せるロードマップ画像が必要でした。スプレッドシートを貼るより、ちゃんとした図が欲しかった。

自分で作った historymap があります。YAML を渡すと10種類のレイアウトでタイムラインHTMLを生成するツールで、iframe 埋め込みまで対応しています。「これを使えばいい」と思ったのですが、そこから詰まりました。

まず、LLM に画像を生成させようとすると HTML しか出ません。PNG が欲しいのに。

次に、引数でデータファイルのパスを渡せません。data.yaml がリポジトリルートにある前提の設計になっていました。

最後に、node src/build.mjs --help と打ったら、ヘルプも何も出さずにビルドが走りました。--format png を試しても同じです。どんなフラグを渡しても、黙って dist/index.html を生成して終わります。自分で作ったツールのはずなのに、何ができて何ができないか、ソースを読まないとわからない状態でした。

MCP サーバーを足すことにした

LLM から使えるようにするなら、MCP サーバーが一番きれいです。generate_timeline(yaml, layout: "skyline", format: "png") を呼べば PNG が返ってくる、という状態にしたかった。

historymap はすでに YAML → HTML の変換を持っています。Puppeteer でスクリーンショットを撮れば PNG になります。あとは MCP ツールとして公開するだけです。追加したコンポーネントは3つでした。

src/screenshot.mjs    HTML → PNG(Puppeteer)
mcp/handlers.mjs      ツールロジック(テスト可能に分離)
mcp/server.mjs        MCP サーバー本体(@modelcontextprotocol/sdk)

設計で迷った2点

1. yaml(文字列)か yamlPath(ファイルパス)か

最初は YAML を文字列で渡す設計にしました。

{ "tool": "generate_timeline", "yaml": "title: ...\nitems:\n  ..." }

実際に使い始めると、100行を超えたあたりから LLM がメッセージの中に YAML を書くのがつらくなります。「この項目を直してもう一度生成して」というやり取りを繰り返すと、毎回 YAML 全文を送ることになります。ファイルを編集してパスだけ渡す方が、会話が軽くなりました。

最終的に両方を受け付けて排他にしました。

generate_timeline({ yaml: "...", layout: "skyline", format: "png" })
generate_timeline({ yamlPath: "/path/to/data.yaml", format: "png" })

ここで一つバグを出しました。yaml: "" を渡したとき、!yaml の判定だと空文字が falsy になって「yaml も yamlPath も指定なし」というエラーに飛んでしまいます。yaml === undefined で判定しないといけません。

2. Puppeteer をどこに置くか

Puppeteer は Chrome ごとダウンロードするので dependencies に入れると常に重い。でも入れなければ PNG が出ない。

optionalDependencies にして動的 import で包む形にしました。

let puppeteer;
try {
  puppeteer = (await import("puppeteer")).default;
} catch {
  throw new Error(
    "PNG export requires puppeteer, but it is not installed.\n" +
    "  Install:  npm install puppeteer\n" +
    "  If you used --omit=optional, re-run without those flags.\n" +
    "  Or set PUPPETEER_EXECUTABLE_PATH to an existing Chrome binary."
  );
}

ここには罠があります。CI で npm install --production--omit=optional をしていると、Puppeteer がインストールされません。HTML は正常に出力されるので、PNG が出なくても最初は気づきません。エラーメッセージに --omit=optional の言及を入れたのはそのためです。

Chrome が入っているが Puppeteer のバンドル版とバージョンが合わない環境では、PUPPETEER_EXECUTABLE_PATH で上書きできます。

PUPPETEER_EXECUTABLE_PATH=/usr/bin/google-chrome-stable \
  node src/cli.mjs --format png --data ./roadmap.yaml

CLI を作り直した

MCP を足すついでに、CLI 自体も parseArgs で作り直しました。

変更前の状態は --all フラグだけ存在して、他は全部黙殺するというものでした。--help もエラーも出ないので、ソースを読まないと何ができるかわかりません。

# 変更後
node src/cli.mjs --data ./roadmap.yaml --layout skyline --format png --width 1400
node src/cli.mjs --help          # usage を表示
node src/cli.mjs --unknown-flag  # エラー + usage 表示

parseArgsstrict: true を指定するだけで未知フラグはエラーになります。--helpreturn だけで exit 0 になりますが、最初 process.exitCode = 1 を書き忘れていて exit 1 になり、&& チェーンが止まって気づきました。

実際に使ったら3つ壊れていた

MCP とCLIが動くようになって、改めてロードマップ画像を作り始めました。そこから先が本番でした。

1. skyline の description が出ない

skyline レイアウトで4つのマイルストーンを並べて画像を生成しました。送る直前に気づいたのですが、description フィールドが全部消えていました。

- date: "2026-08-02"
  title: "認証・iframe"
  subtitle: "8/2 〜 8/10週"
  description: "認証実装・iframe埋め込み・簡易サイト確認"
  # ↑ これが画像に出ていなかった

skyline レンダラーが titlesubtitle しか描画していませんでした。description フィールドを追加して、track の外に overflow する形で表示するようにしました。overflow: hidden でクリップする設計にすると情報が消えるので、はみ出して読める方を選びました。

2. 同月に2つ入れると日付ラベルが重複する

7月に「管理画面・連携(7/6週)」と「実機連携(7/19週)」の2つを入れたら、2026.07 が横に2つ並びました。

2026.07   2026.07   2026.08   2026.08
  ↑         ↑
  同じラベルが2つ

連続する同一 displayLabel を非表示にするだけで直ります。visibility: hidden を使えばレイアウト上のスペースを保ちながら消せます。

3. 日本語と半角記号の折り返しが変

生成した PNG を見たら、「要件定義 →」の が次の行の先頭に来ていました。「実装)」の が単独行になっているケースもありました。顧客に送る前に気づいてよかったです。

.skyline-content {
  line-break: strict;
  word-break: keep-all;
  overflow-wrap: break-word;
}

line-break: strict は CJK テキストの行末禁則処理を厳格にします。閉じ括弧や矢印が行頭に来なくなります。

完成後

skyline レイアウト、width 1400、description あり、ラベル重複除去後の画像です。

開発ロードマップ skyline layout

Claude Code から MCP 経由で使う場合は .mcp.json に以下を追加します。

{
  "mcpServers": {
    "historymap": {
      "command": "node",
      "args": ["/path/to/historymap/mcp/server.mjs"],
      "env": {
        "PUPPETEER_EXECUTABLE_PATH": "/usr/bin/google-chrome-stable"
      }
    }
  }
}

「このプロジェクトのロードマップを skyline で PNG にして」と伝えれば、LLM が YAML を書いてツールを呼んで画像を返します。

振り返り

MCP を既存 OSS に後付けするのは、新規開発よりコストが低いです。historymap は YAML → HTML がすでに動いていたので、追加は HTML → PNG と MCP ツール定義だけでした。

ただ今回改めて感じたのは、「自分で作ったものでも、実際に使い始めるまで見えない問題がある」ということです。CLI の黙殺、description の欠落、ラベルの重複、折り返しの崩れ。どれも開発中には出てきませんでした。クライアント向けの資料を作ろうとして初めて見えました。

コード全体は github.com/kenimo49/historymap にあります。


この記事で扱った実装は CHANGELOG.md にまとめています。