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 表示
parseArgs の strict: true を指定するだけで未知フラグはエラーになります。--help は return だけで 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 レンダラーが title と subtitle しか描画していませんでした。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 あり、ラベル重複除去後の画像です。

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 にまとめています。
この記事は役に立ちましたか?