Skip to content

Commit c0f51e2

Browse files
kitepon-rgbclaude
andcommitted
feat(openai-image): upstream を HermesAgent ラッパへ切替 (OpenAI 課金経路を撤去)
- openai-image-mcp コンテナの中身を kazyam53/openai_gen_image_mcp から自前の fastmcp ラッパ (server.py) に置換し、HermesAgent の `generate_image` MCP を 上流に呼ぶ薄い proxy に。OpenAI 課金経路を完全に切り離す。 - service 名 / volume 名 / 出力 path 形式 / クライアント側 URL / image-hub-app の intercept.ts REWRITE_RULES key は互換維持。tool schema のみ HermesAgent 互換 に変更 (`prompt` / `aspect_ratio` / `resolution` / `quality(bool)`)。 - 環境変数: OPENAI_API_KEY を廃止、HERMES_MCP_URL と HERMES_BEARER_TOKEN を追加。 - sitecustomize.py は server.py 内の inline `os.chmod(sub, 0o755)` で代替し削除。 - code-reviewer findings 取り込み: *.x.ai のみ allow する SSRF guard、未知 MIME での silent failure 回避 (intercept regex 不一致による .bin 化を阻止)、起動時 env 不足を SystemExit で明示。 - tool docstring を比較表現排除 + 絶対指標 (USE FOR / DON'T USE FOR / Args / Returns / Limits) のみのスタイルに再構成。 - ドキュメント更新: PLAN §7 として履歴化、server/README + README.md + README.ja.md の OpenAI 前提文言を「上流ラッパ」中立記述に。 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 6dbfdef commit c0f51e2

9 files changed

Lines changed: 297 additions & 70 deletions

File tree

CLAUDE.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,12 +39,12 @@ Windows 側に stdio で登録されていた画像/作図系 MCP サーバー 3
3939
- **transport は Streamable HTTP (`type: "http"`)**: SSE は採用しない (リバプロ越しで messages の relative URL が崩れて死ぬ — memory `feedback_mcp_proxy_streamable_http` 参照)。
4040
- **リバプロは `node:fetch` 直叩き**: `http-proxy-middleware` v3 + Express 5 の組合せは silent fail を起こす。`server/image-hub-app/src/index.ts``app.all(mcpPath, bearer, async ...)` パターンを踏襲する。
4141
- **chromium を含むコンテナは `--no-sandbox` 強制**: root 実行を拒否されるので Dockerfile で `/usr/bin/chromium` をラッパに差し替えて `--no-sandbox --disable-dev-shm-usage` を強制注入する (`server/mermaid-mcp/Dockerfile` 参照)。
42-
- **`OPENAI_API_KEY` 露出履歴**: 旧鍵がこの計画の元になったチャットで露出済み → Phase 3-2 で再発行済 (project key 名 `image-hub`)。新規にコード/設定例を書くときも旧鍵を使い回さない
42+
- **openai-image MCP の upstream は HermesAgent に切替済** (2026-05-18): OpenAI 課金から脱却するため、`server/openai-image-mcp/` の中身を kazyam53/openai_gen_image_mcp から自前の HermesAgent ラッパ ([server.py](server/openai-image-mcp/server.py)) に差し替えた。service 名・MCP 名・volume 名 (`openai-image` / `openai-image-tmp`) と出力 path 形式 (`/var/lib/openai-image-tmp/openai_gen_image_*/generated_*.{png,jpg,webp}`) は intercept.ts の REWRITE_RULES に紐付くため互換維持。billing path は HermesAgent 側の SuperGrok / Premium Plus OAuth 経由で課金ゼロ。`OPENAI_API_KEY` は不要になり `.env.example` からも削除済 (旧鍵は Phase 3-2 で revoke 済)。新規 env は `HERMES_MCP_URL``HERMES_BEARER_TOKEN`。tool schema が変更されたため (旧: `size`/`n`/`quality(str)` → 新: `aspect_ratio`/`resolution`/`quality(bool)`) クライアント側プロンプトの引数指定があれば見直す
4343
- **`.env` は絶対 commit しない**: `server/.gitignore` で除外済。バックアップにも `.env*` exclude (Day-1 で実機検証済)。
4444
- **claude-spotter で WSL2 から Windows 側 MCP を収集する場合**: 1.2.2 以上を使う。
4545
- **`/mcp/<name>` は 3 経路の認可** (2026-05-04 追加): (1) JSON-RPC method が discovery 系 (`initialize` / `tools/list` / `prompts/list` / `resources/list` / `resources/templates/list` / `notifications/initialized` / `notifications/cancelled` / `ping`) なら bearer 不要 (2) `Authorization: Bearer ${IMAGEHUB_STATIC_BEARER_TOKEN}` 一致なら OAuth 検証 skip (3) それ以外は従来の OAuth bearer 検証。理由: Spotter 等の外部 catalog 消費者は OAuth トークンを持てないので `tools/list` が 401 で取れない / Bell のような OAuth フロー回せない隔離 Claude も救う必要があった。実装は [server/image-hub-app/src/index.ts](server/image-hub-app/src/index.ts) の `mcpAuth` ミドルウェア。静的トークンは `IMAGEHUB_STATIC_BEARER_TOKEN` を `.env` に置く (32 文字以上、空なら経路 (2) は無効化)。漏洩時のローテは `openssl rand -hex 32` で再発行 → ローカル + prod `.env` 両方更新 → `docker compose up -d --build image-hub`。
4646
- **`/files/<id>``mcpAuth` で受ける** (2026-05-04 後追い): 元は OAuth bearer 限定だったが、Bell 等の静的 bearer 消費者から取得できるよう同 middleware に統一。`/files/<id>` は GET only で req.body は undefined なので discovery method 例外は発火せず安全。
47-
- **proxy intercept で生成物を `/files/<id>` URL に rewrite** (2026-05-04 追加): openai-image MCP のような「コンテナ内 tmp に生成物を保存して絶対パスを返す」 上流に対応するため、`/mcp/<name>` proxy が `tools/call` レスポンスを streaming SSE Transform で chunk 単位 parse → text content 中の `/var/lib/openai-image-tmp/openai_gen_image_*/...` を検出 → SHA256 prefix12 を id に storage 格上げ + artifacts DB 登録 → text を `https://image-hub.kitepon.dynv6.net/files/<id>.<ext>` URL に rewrite して re-emit。`Image` content (base64) は不変 (LLM が画像を見て会話できる必要があるため)。実装は [server/image-hub-app/src/intercept.ts](server/image-hub-app/src/intercept.ts) の `REWRITE_RULES` (現状 openai-image のみ、 mermaid・excalidraw は path pattern 確認後に追加)。openai-image-mcp は `TMPDIR=/var/lib/openai-image-tmp` を named volume `openai-image-tmp` で image-hub-app と共有 (ro マウント) する構成 ([compose.yml](server/compose.yml))。Python `tempfile.mkdtemp` が固定 0o700 で dir 作成するため [openai-image-mcp/sitecustomize.py](server/openai-image-mcp/sitecustomize.py) で monkey-patch して 0o755 に補正 (image-hub-app uid 1000 から ro 参照するため)。
47+
- **proxy intercept で生成物を `/files/<id>` URL に rewrite** (2026-05-04 追加): openai-image MCP のような「コンテナ内 tmp に生成物を保存して絶対パスを返す」 上流に対応するため、`/mcp/<name>` proxy が `tools/call` レスポンスを streaming SSE Transform で chunk 単位 parse → text content 中の `/var/lib/openai-image-tmp/openai_gen_image_*/...` を検出 → SHA256 prefix12 を id に storage 格上げ + artifacts DB 登録 → text を `https://image-hub.kitepon.dynv6.net/files/<id>.<ext>` URL に rewrite して re-emit。`Image` content (base64) は不変 (LLM が画像を見て会話できる必要があるため)。実装は [server/image-hub-app/src/intercept.ts](server/image-hub-app/src/intercept.ts) の `REWRITE_RULES` (現状 openai-image のみ、 mermaid・excalidraw は path pattern 確認後に追加)。openai-image-mcp は `TMPDIR=/var/lib/openai-image-tmp` を named volume `openai-image-tmp` で image-hub-app と共有 (ro マウント) する構成 ([compose.yml](server/compose.yml))。Python `tempfile.mkdtemp` が固定 0o700 で dir 作成するため、新しい [openai-image-mcp/server.py](server/openai-image-mcp/server.py) は mkdtemp 直後に `os.chmod(sub, 0o755)` を明示的に呼ぶ (image-hub-app uid 1000 から ro 参照するため)。旧構成の sitecustomize.py monkey-patch は不要になり削除済。
4848
- **mcp-proxy の Streamable HTTP は long-lived stream** (caveat `mcp-proxy-streamable-http-sse-await-fetch-arraybuffer-undici-bodytimeout-5min-crash`): tool 結果を 1 イベント送った後も SSE 接続を閉じない仕様。proxy 側で `await upstreamRes.arrayBuffer()` 等の「全 body を受けてから返す」 コードを書くと undici の `bodyTimeout: 300_000` (5 min) で必ず crash → docker restart loop に入る。intercept.ts の `makeSseRewriteTransform()` のように **chunk 単位 Transform で stream を閉じずに pipe しっぱなし** が正解。Web→Node Stream 変換 (`Readable.fromWeb`) の error event は pipe で自動伝搬しないので両端に明示的な error handler を付けないと Node プロセス即死する点にも注意。
4949

5050
## デプロイ

README.ja.md

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -46,15 +46,15 @@ After (このプロジェクト):
4646
- 3 本の stdio MCP を [`mcp-proxy`](https://www.npmjs.com/package/mcp-proxy) で HTTP 化したコンテナ
4747
- 前段に Caddy (TLS)
4848

49-
結果: 新しいマシンに Claude Code を入れる → URL 3 行貼る → 1 度 OAuth する → 3 つとも動く。`OPENAI_API_KEY` はサーバーだけ
49+
結果: 新しいマシンに Claude Code を入れる → URL 3 行貼る → 1 度 OAuth する → 3 つとも動く。上流認証情報はサーバーだけに置く
5050

5151
## なぜ stdio MCP のままじゃダメなのか
5252

5353
| 観点 | マシンごと stdio | このプロジェクト (HTTP ハブ) |
5454
|---|---|---|
55-
| `OPENAI_API_KEY` の置き場所 | 全マシン | サーバー `.env` のみ |
55+
| 上流認証情報の置き場所 | 全マシン | サーバー `.env` のみ |
5656
| 新しいマシンを追加 | stdio パッケージ 3 本再インストール、鍵を貼り直す、Chromium も再配布 | URL 3 行貼って OAuth 1 回 |
57-
| 鍵ローテ | 全マシン同時に更新 | サーバー `.env` 1 か所差し替え |
57+
| 認証情報ローテ | 全マシン同時に更新 | サーバー `.env` 1 か所差し替え |
5858
| モバイル / SSH / 出先 | 痛い (Chromium も Node もない) | `Claude Code` と URL だけ |
5959
| マシンあたり常駐 stdio プロセス数 | 3 (アイドルでも常駐) | 0 (HTTP、必要時のみ) |
6060

@@ -77,7 +77,7 @@ flowchart LR
7777
caddy -->|reverse_proxy| hub
7878
subgraph host["Docker Compose ホスト"]
7979
hub[image-hub-app<br/>OAuth 2.1 +<br/>fetch ベース proxy]
80-
hub --> oai["openai-image-mcp<br/>(mcp-proxy + uv tool)"]
80+
hub --> oai["openai-image-mcp<br/>(mcp-proxy + 上流ラッパ)"]
8181
hub --> exc["excalidraw-mcp<br/>(mcp-proxy + npm)"]
8282
hub --> mer["mermaid-mcp<br/>(mcp-proxy + claude-mermaid)"]
8383
end
@@ -100,7 +100,8 @@ cp .env.example .env
100100
# IMAGEHUB_PUBLIC_AUTH_URL=https://image-hub.example.com
101101
# IMAGEHUB_OAUTH_SIGNING_KEY=$(openssl rand -base64 64)
102102
# IMAGEHUB_ADMIN_PASSCODE=$(openssl rand -base64 18)
103-
# OPENAI_API_KEY=sk-proj-...
103+
# HERMES_MCP_URL=<openai-image の上流 MCP エンドポイント>
104+
# HERMES_BEARER_TOKEN=<その上流の静的 bearer>
104105

105106
cat caddy/image-hub.snippet >> /path/to/your/Caddyfile
106107
docker compose up -d --build
@@ -139,7 +140,7 @@ OAuth 注意: **同一 MCP サーバー** に対して 2 つのフロー (例: V
139140
│ └── PHASE2A-client-cutover.md # クライアント切替手順
140141
└── server/
141142
├── image-hub-app/ # Express OAuth + reverse proxy
142-
├── openai-image-mcp/ # mcp-proxy + openai-gen-image-mcp
143+
├── openai-image-mcp/ # mcp-proxy + 自前 Python ラッパ (HermesAgent 上流に proxy)
143144
├── excalidraw-mcp/ # mcp-proxy + mcp-excalidraw-server
144145
├── mermaid-mcp/ # mcp-proxy + claude-mermaid (chromium 修正込み)
145146
├── caddy/image-hub.snippet # リバプロホストブロック
@@ -151,6 +152,7 @@ OAuth 注意: **同一 MCP サーバー** に対して 2 つのフロー (例: V
151152

152153
- **Phase 2.A** (デプロイ + 集約): 完了
153154
- **Phase 3-2** (鍵ローテ): 完了
155+
- **Phase 3-X** (openai-image 上流を HermesAgent に切替、2026-05-18): 完了 — [docs/PLAN-mcp-image-hub.md §7](docs/PLAN-mcp-image-hub.md) 参照
154156
- **Week-2 ガード** (クライアント別レート制限、予算アラート): 進行中
155157
- **Phase 2.B** (`/gallery``/dashboard`、content-hash キャッシュ): 計画
156158
- **Phase 4** (パイプライン MCP、画像→プロンプト vision MCP、fal.ai 層): 計画
@@ -161,7 +163,7 @@ OAuth 注意: **同一 MCP サーバー** に対して 2 つのフロー (例: V
161163

162164
- [`mcp-proxy`](https://github.com/punkpeye/mcp-proxy) — stdio→HTTP の核
163165
- [`@modelcontextprotocol/sdk`](https://github.com/modelcontextprotocol) — OAuth 2.1 サーバープリミティブ
164-
- [`claude-mermaid`](https://github.com/veelenga/claude-mermaid) / [`mcp-excalidraw-server`](https://github.com/yctimlin/mcp_excalidraw) / [`openai_gen_image_mcp`](https://github.com/kazyam53/openai_gen_image_mcp) — 集約対象の上流 stdio MCP
166+
- [`claude-mermaid`](https://github.com/veelenga/claude-mermaid) / [`mcp-excalidraw-server`](https://github.com/yctimlin/mcp_excalidraw) — 集約対象の上流 stdio MCP
165167

166168
## License
167169

README.md

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -46,15 +46,15 @@ Server-side (one host, Docker Compose):
4646
- 3 stdio MCP servers wrapped via [`mcp-proxy`](https://www.npmjs.com/package/mcp-proxy) into HTTP containers
4747
- Caddy out front for TLS
4848

49-
Result: install Claude Code on a new laptop → paste 3 URLs → run OAuth flow once → all three image/diagram MCPs work. The `OPENAI_API_KEY` lives only on the server.
49+
Result: install Claude Code on a new laptop → paste 3 URLs → run OAuth flow once → all three image/diagram MCPs work. Upstream credentials live only on the server.
5050

5151
## Why not just use stdio MCPs everywhere?
5252

5353
| Concern | Per-machine stdio | This project (HTTP hub) |
5454
|---|---|---|
55-
| `OPENAI_API_KEY` location | Every laptop | One server `.env` only |
55+
| Upstream credential location | Every laptop | One server `.env` only |
5656
| Onboard a new machine | Reinstall 3 stdio packages, re-paste keys, re-distribute Chromium | Paste 3 URLs, run OAuth once |
57-
| Key rotation | Update every laptop simultaneously | Change `.env` on the server |
57+
| Credential rotation | Update every laptop simultaneously | Change `.env` on the server |
5858
| Mobile / SSH / remote | Painful (no Chromium, no Node) | Just `Claude Code` + the URLs |
5959
| Total stdio processes spawned per machine | 3 (always running, even idle) | 0 (HTTP, on-demand) |
6060

@@ -77,7 +77,7 @@ flowchart LR
7777
caddy -->|reverse_proxy| hub
7878
subgraph host["Docker Compose host"]
7979
hub[image-hub-app<br/>OAuth 2.1 server +<br/>fetch-based reverse proxy]
80-
hub --> oai["openai-image-mcp<br/>(mcp-proxy + uv tool)"]
80+
hub --> oai["openai-image-mcp<br/>(mcp-proxy + upstream wrapper)"]
8181
hub --> exc["excalidraw-mcp<br/>(mcp-proxy + npm)"]
8282
hub --> mer["mermaid-mcp<br/>(mcp-proxy + claude-mermaid)"]
8383
end
@@ -100,7 +100,8 @@ cp .env.example .env
100100
# IMAGEHUB_PUBLIC_AUTH_URL=https://image-hub.example.com
101101
# IMAGEHUB_OAUTH_SIGNING_KEY=$(openssl rand -base64 64)
102102
# IMAGEHUB_ADMIN_PASSCODE=$(openssl rand -base64 18)
103-
# OPENAI_API_KEY=sk-proj-...
103+
# HERMES_MCP_URL=<your upstream MCP endpoint for openai-image>
104+
# HERMES_BEARER_TOKEN=<static bearer for that upstream>
104105

105106
cat caddy/image-hub.snippet >> /path/to/your/Caddyfile
106107
docker compose up -d --build
@@ -141,7 +142,7 @@ Plus an OAuth gotcha: don't fire two OAuth flows for the **same** MCP server sim
141142
│ └── PHASE2A-client-cutover.md # Client setup guide
142143
└── server/
143144
├── image-hub-app/ # Express OAuth + reverse proxy
144-
├── openai-image-mcp/ # mcp-proxy + openai-gen-image-mcp
145+
├── openai-image-mcp/ # mcp-proxy + thin Python wrapper (proxies to HermesAgent upstream)
145146
├── excalidraw-mcp/ # mcp-proxy + mcp-excalidraw-server
146147
├── mermaid-mcp/ # mcp-proxy + claude-mermaid (with chromium fix)
147148
├── caddy/image-hub.snippet # Reverse proxy host block
@@ -153,6 +154,7 @@ Plus an OAuth gotcha: don't fire two OAuth flows for the **same** MCP server sim
153154

154155
- **Phase 2.A** (deploy + aggregate): complete
155156
- **Phase 3-2** (key rotation): complete
157+
- **Phase 3-X** (openai-image upstream cutover to HermesAgent, 2026-05-18): complete — see [docs/PLAN-mcp-image-hub.md §7](docs/PLAN-mcp-image-hub.md)
156158
- **Week-2 guards** (per-client rate limit, budget alerts): in progress
157159
- **Phase 2.B** (`/gallery`, `/dashboard`, content-hash cache): planned
158160
- **Phase 4** (pipeline MCP, image→prompt vision MCP, fal.ai layer): planned
@@ -162,7 +164,7 @@ See [docs/PLAN-mcp-image-hub.md §2](docs/PLAN-mcp-image-hub.md) for the full ch
162164
<details>
163165
<summary>Why this repo exists (origin story)</summary>
164166

165-
The author was running 3 image-related stdio MCPs on Windows — `openai-image`, `excalidraw`, `mermaid` — and wanted them available from WSL2, SSH sessions, mobile, and friends' machines without copying the OpenAI key (and the Chromium dependency for mermaid) to every device.
167+
The author was running 3 image-related stdio MCPs on Windows — `openai-image`, `excalidraw`, `mermaid` — and wanted them available from WSL2, SSH sessions, mobile, and friends' machines without copying upstream credentials (and the Chromium dependency for mermaid) to every device.
166168

167169
A few searches turned up no off-the-shelf "MCP gateway with OAuth + reverse proxy + per-server transport translation" project, so this is one. The OAuth implementation pattern is borrowed from a sibling Relay-MCP project; the rest is fresh.
168170

@@ -172,7 +174,7 @@ A few searches turned up no off-the-shelf "MCP gateway with OAuth + reverse prox
172174

173175
- [`mcp-proxy`](https://github.com/punkpeye/mcp-proxy) — the stdio→HTTP wrapping that makes any of this possible
174176
- [`@modelcontextprotocol/sdk`](https://github.com/modelcontextprotocol) — OAuth 2.1 server primitives
175-
- [`claude-mermaid`](https://github.com/veelenga/claude-mermaid), [`mcp-excalidraw-server`](https://github.com/yctimlin/mcp_excalidraw), [`openai_gen_image_mcp`](https://github.com/kazyam53/openai_gen_image_mcp) — the upstream stdio MCPs being aggregated
177+
- [`claude-mermaid`](https://github.com/veelenga/claude-mermaid), [`mcp-excalidraw-server`](https://github.com/yctimlin/mcp_excalidraw) — the upstream stdio MCPs being aggregated
176178

177179
## License
178180

0 commit comments

Comments
 (0)