Skip to main content
Glama
Weone404

weone-daily-post

by Weone404

weone-daily-post — リモートMCPサーバー

We One Aviation の毎日の Instagram + Facebook 投稿用の公開バックエンドです。 これはステートレスな Streamable HTTP MCP サーバーです。 Claude が思考(トピック選定、文言、キャプション)を行い、このサービスが副作用(履歴、画像レンダリング、ストレージ、Graph API)を担当します。画像は組版であり、生成ではありません。ポスター上のテキストは、指定されたテキストと正確に一致します。

Claude ──POST /mcp (Bearer)──▶ Render web service (Node 20, Express)
                                 ├─ Supabase  posts table + post-images bucket
                                 ├─ Chromium      HTML template → JPEG
                                 └─ Meta Graph  IG container/publish, FB photos

ツール

ツール

目的

get_past_topics()

新しい順の履歴、最大200行: {id, topic, category, status, created_at}。トピックを選ぶ前にこれを読んでください。

reserve_topic(topic, category)

status='reserved' を挿入し、{id} を返します。category は news、subject、career のいずれか。繰り返しのトピックは duplicate_topic で失敗します。

render_post({template, headline, points, footer?, eyebrow?, slug?})

ブランド化された HTML テンプレートを正確なサイズの sRGB JPEG にレンダリングし、アップロードして、画像ブロックと公開 URL を返します。

publish_socials(image_url, caption, hashtags, history_id)

Instagram に公開し、次に Facebook に公開し、結果を記録します。

mark_draft(history_id, image_url, caption)

シャドウモード: 公開せずに完成した投稿を draft として記録します。

check_token()

Meta トークンの有効期限までの日数と付与されたスコープ。

すべてのツールは JSON を返します。成功は {"ok": true, ...}、失敗は {"ok": false, "error": {code, message, retryable, details}} を含む MCP エラー結果です。呼び出し元に生のスタックトレースが投げられることはありません。

render_post

template は news、subject、career のいずれか — posts テーブルが使用するのと同じ3つのカテゴリです。

フィールド

制限

備考

headline

60文字

Barlow 700、最大3行。文の大文字小文字(タイトルケースではありません)。

points

3〜4項目、各90文字

Barlow 400、各項目に金色のマーカー

eyebrow

32文字、任意

金色、CSS で大文字化、例: NAVIGATION

footer

90文字、任意

フッターバーの左側、例: DGCA · 14 Aug 2026

これは2つのコンテンツブロックを返します: 画像ブロック(base64 JPEG)と、公開 URL、ファイル名、寸法、バイトサイズ、レンダリング時間を含むテキストブロックです。

アップロード前に2つのガードが実行され、どちらも問題のあるフィールドを指名します:

  1. 長さ制限 — Chromium に触れる前にチェックされる、安価な拒否です。 points[2] is 97 characters, limit is 90. Shorten it and retry.

  2. ページ内計測 — レイアウト後、すべてのテキストボックスは固定サイズのクリッピングボックスであり、その内容がボックスより縦または横に大きい場合、レンダリングはフィールド名とピクセル単位のオーバーフローとともに拒否されます。これは文字数では見えない問題、例えば長さは合法だが端からはみ出す分割不可能な80文字のトークンを捕捉します。

どちらのガードが発動しても何もアップロードされないため、拒否にかかるコストは1秒で、修正は常に「指名されたフィールドを短くする」ことです。

画像ブロックは文脈で文言を確認できるように返されますが、もはや正しさのチェックではありません。固定テンプレートは単語を誤綴したり、図をでっち上げたりすることはできません。最悪の合法入力(58文字の見出しと4つの90文字のポイント、最も広い合法な eyebrow と footer)は、3つのテンプレートすべてに収まることが検証されています。

publish_socials、ステップバイステップ

  1. image_url に HEAD を送り、200 と content-type: image/jpeg を確認します。Meta はこの URL をサーバーサイドで取得するため、不正な URL は数時間後に不透明に失敗します。(HEAD を拒否するストアには、1バイトの範囲 GET が代わりに送信されます。)

  2. Instagram — full_caption = caption + "\n\n" + hashtags.join(' ')、2200文字に制限されます。ハッシュタグのみが末尾から削除されます。キャプション本文は決して切り詰められません。本文だけが2200文字を超える場合、何も投稿される前に caption_too_long で呼び出しが失敗します。 POST {IG_USER_ID}/media → GET {container}?fields=status_code,status を1秒に1回、最大60秒間ポーリング → FINISHED の場合のみ公開します。ERROR の場合、status 文字列がそのまま返されます。なぜなら、Meta が何を嫌ったかを説明する唯一の場所だからです。

  3. Facebook — POST {FB_PAGE_ID}/photos を url と message 付きで実行します。Instagram の結果に関係なく試行されます。

  4. 記録 — posts 行に ig_post_id、fb_post_id、image_url、および status = published(両方)、partial(片方)、failed(どちらも)が設定されます。

{ig_post_id, fb_post_id, status, errors: [...]} を返します。単一プラットフォームの失敗は決して飲み込まれません。errors[] にプラットフォーム、失敗したステージ、Meta 自身の code / error_subcode / message とともに表示されます。

Related MCP server: Meta Social MCP

環境変数

変数

必須

説明

MCP_AUTH_TOKEN

はい

/mcp 用の共有シークレット。コネクタは Authorization: Bearer <値> を送信する必要があります。未設定の場合、サーバーは起動して /health を提供しますが、すべての /mcp リクエストを 500 で拒否します — フェイルクローズドであり、フェイルオープンではありません。node -e "console.log(require('crypto').randomBytes(32).toString('hex'))" で生成してください。

SUPABASE_URL

はい

https://<project-ref>.supabase.co。

SUPABASE_SERVICE_KEY

はい

サービスロールキー。RLS をバイパスします — サーバーサイドのみ。コネクタ設定には絶対に置かないでください。

META_GRAPH_VERSION

いいえ(デフォルト v23.0)

すべての呼び出しに使用される Graph API バージョン。

IG_USER_ID

公開用

Instagram ビジネスアカウント ID(数値、@ハンドルではありません)。

FB_PAGE_ID

公開用

その Instagram アカウントにリンクされた Facebook ページ ID。

META_PAGE_ACCESS_TOKEN

公開用

instagram_basic、instagram_content_publish、pages_show_list、pages_read_engagement、pages_manage_posts を持つ長期有効なページアクセストークン。約60日で期限切れになります — check_token() で残り日数がわかります。

PORT

いいえ

Render がこれを設定します。デフォルト 10000。

MAX_INLINE_IMAGE_BYTES

いいえ(デフォルト 1400000)

このサイズを超えると、インラインの base64 プレビューがダウンスケールされます。

CHROMIUM_EXECUTABLE_PATH

いいえ

Chrome/Chromium バイナリへの明示的なパス。プラットフォームごとのデフォルトを上書きします。

CHROMIUM_SINGLE_PROCESS

いいえ

1 に設定すると --single-process を強制します。ブラウザの再利用が犠牲になります — 起動ごとに1回のレンダリングです。ブラウザのライフサイクル を参照してください。

ローカル実行用に .env.example を .env にコピーしてください。.env は gitignore されています — そのままにしてください。

セットアップ

1. Supabase

SQL エディタで migrations/001_init.sql を実行してください(または supabase db push)。これは冪等であり、以下を作成します:

  • チェック制約と topic の一意インデックスを持つ posts テーブル — そのインデックスが繰り返しガードであるため、重複予約は 意図的に 失敗します、

  • created_at desc と status のインデックス、

  • ポリシーなしで posts に RLS を有効化(サービスキーのみがアクセス可能)、

  • 公開の post-images ストレージバケットとその公開読み取りポリシー。 公開読み取りは必須です: Meta は JPEG を自分で取得し、資格情報を提示できません。

2. Meta

Facebook ページにリンクされた Instagram ビジネスまたはクリエイターアカウントと、上記のスコープを持つ長期有効なページトークンが必要です。初回実行前に check_token() で確認してください — 期限切れトークンは朝の失敗の最も一般的な原因です。

3. Render にデプロイ

render.yaml(Blueprint)を使用:

  1. このリポジトリを GitHub にプッシュします。

  2. Render ダッシュボード → New → Blueprint → リポジトリを選択します。render.yaml を読み取ります: Node 20、npm ci && npm run build、npm start、/health でのヘルスチェック。

  3. Render は sync: false のすべての変数を要求します。それらを貼り付けます。

  4. デプロイし、ログで server.listening ... auth=configured を確認します。auth=MISSING は MCP_AUTH_TOKEN が設定されなかったことを意味します。

手動:

  1. 新規 → Web Service → リポジトリを接続します。

  2. ランタイム Node、ビルド npm ci && npm run build、起動 npm start。

  3. ヘルスチェックパス /health。

  4. 上記の表の環境変数に加えて、NODE_VERSION=20 を追加します。

検証:

curl https://<your-service>.onrender.com/health
# {"status":"ok","server":{...},"tools":[...six...],"uptime_s":3}

無料プランではなく Starter プランを使用してください。 Chromium は Node に加えて約 400 MB の常駐メモリを必要とし、無料インスタンスは 512 MB です — レンダリング中に OOM が発生し、その障害は有用なログ行ではなくワーカーの停止として現れます。無料プランはまた、非アクティブ後にスリープするため、その日の最初のツール呼び出しには 30〜60 秒のコールドスタートが追加でかかります。render.yaml は両方の理由から starter を設定しています。

ビルド時にブラウザのダウンロードは不要です: @sparticuz/chromium は依存関係として独自のバイナリを同梱しているため、npm ci && npm run build がビルド全体です。このビルドステップは src/templates/ を dist/ にコピーします — tsc は .ts のみを出力するため、これがないとサーバーは正常に起動しますが、最初のレンダリングでテンプレートファイルが見つからず失敗します。

4. Claude に接続する

エンドポイントは:

https://<your-service>.onrender.com/mcp

ヘッダー付き:

Authorization: Bearer <MCP_AUTH_TOKEN>

Claude Code / Cowork CLI:

claude mcp add --transport http weone-social \
  https://<your-service>.onrender.com/mcp \
  --header "Authorization: Bearer <MCP_AUTH_TOKEN>"

.mcp.json(プロジェクトスコープ、トークンなしでチェックイン):

{
  "mcpServers": {
    "weone-social": {
      "type": "http",
      "url": "https://<your-service>.onrender.com/mcp",
      "headers": { "Authorization": "Bearer ${MCP_AUTH_TOKEN}" }
    }
  }
}

Claude デスクトップ/Web のカスタムコネクタダイアログで、同じ /mcp URL を貼り付け、ベアラートークンをリクエストヘッダーフィールドに入れます。認証は設計上ヘッダーのみです — トークンがクエリパラメータとして受け入れられることは決してありません。URL はプロキシログやブラウザ履歴に残るためです。

毎日の実行手順(ブランドルール、禁止クレーム、カテゴリローテーション、画像仕様、QA チェックリスト)は weone-daily-post スキルにあり、このサーバーにはありません。このサービスは意図的に編集ポリシーを保持していません。

ローカル開発

npm install
cp .env.example .env      # fill it in
npm run dev               # tsx watch, http://localhost:10000
npm run typecheck
npm run build && npm start

npm run smoke

npm run smoke                 # render all three templates, upload, print 3 URLs
npm run smoke -- --no-upload  # render locally only, no credentials needed

各テンプレートを 1 つずつレンダリングし、3 つの JPEG すべてを ./out に書き込み、アップロードし、各公開 URL を HEAD チェックして 3 つのリンクを出力します。その後、両方のガードがまだ機能することを証明します。Meta エンドポイントには一切触れないため、本番認証情報に対して安全です。アップロードには SUPABASE_URL と SUPABASE_SERVICE_KEY のみが必要です。--no-upload は何も必要としません。

ローカルファイルはアップロード前に書き込まれるため、Supabase が失敗しても確認できるものが残ります。

レンダリングパイプライン

ヘッドレス Chromium が file:// 経由で src/templates/{template}.html を読み込み、値が DOM に書き込まれ、ページがスクリーンショットされます。同じ入力は常に同じピクセルを生成します。

  • テンプレートは src/templates/ にあります。tokens.css がすべての色を保持し、base.css が 3 つすべてが共有する骨格を保持します。テンプレートファイルが兄弟ファイルと異なるのは、アイブロウの処理とポイントマーカーのみです(ニュース: ゴールドの罫線、科目: 番号付きゴールドの円、キャリア: ゴールドのシェブロン)。

  • フォントはセルフホストされており、src/templates/fonts/ にあります(すべてに Barlow 400/600/700、ワードマークのみに Cinzel 600、ラテンサブセット、OFL)。レンダリング時にフェッチされるものはありません — ネットワーク呼び出しは出力を非決定的にし、Render 上で静かに失敗してシステムのセリフフォールバックになります。レンダラーは document.fonts.ready を待ち、フォールバックをスクリーンショットするのではなく、両方のフェイスが実際に読み込まれたことをアサートします。

  • ユーザーテキストがマークアップに連結されることは決してありません。 値は textContent と createElement を通じて入るため、エスケープを間違えることはありません: 見出しの <script> は文字通りの文字としてポスターに表示されます。

  • ビューポートは deviceScaleFactor: 2 で 1080×1350、スクリーンショットは 2160×2700 でダウンサンプリングされます — テキストのエッジはクリーンなままです。

  • sharp: resize(1080, 1350, {fit:'cover'}) → toColorspace('srgb') → jpeg({quality: 90, chromaSubsampling: '4:4:4'})、メタデータは削除されます。4:4:4 は飾りではありません — 4:2:0 は色付きテキストのエッジをにじませますが、これらのポスターはテキストです。

  • エンコードされた JPEG が 8 MB 未満であることと、デコードされた寸法が実際に要求されたものであることをアサートします。

  • {yyyy-mm-dd}-{slug}-{6 hex}.jpg(UTC 日付)としてアップロードします。すべてのレンダリングは独自のキーを取得し、何も上書きされることはありません — upsert: false。トピックを再レンダリングしても、以前の URL を公開済みの投稿の画像を変更することはできません。cacheControl が 60 秒なのも同じ理由です: 不良オブジェクトは CDN に 1 年間固定される代わりに、1 分以内に修正可能なままです。Meta はアップロード直後にサーバーサイドで URL を 1 回だけフェッチするため、長いキャッシュは不要です。オブジェクトは蓄積されます。ストレージは、ライブ投稿の古い画像よりもはるかに安価です。

レイアウトの動作

タイプはポイント数に応じてスケールします。 3 ポイントでは見出し 68px、本文 36px。4 ポイントでは 60px と 32px。これは CSS の :has() で行われるため、レイアウトの決定は完全にテンプレート内にあり、レンダラーは知ることも気にすることもありません。オーバーフローガードはスケーリング後に実行されるため、スケーリングされた結果が測定対象になります。

コンテンツブロックはヘッダー罫線とフッターバーの間で垂直方向に中央揃えされます。固定ギャップだけではフィルターゲットを保持できません。テキストの量が変わるためです。そのため、3 つの弾性要素が残りのスペースを共有します: 上部のバンド、下部のバンド、見出し下のギャップです。バンドは 150px でハードキャップされ、「大きな空きマージンなし」を強制します。キャップに達すると、余剰は見出しギャップに入り、穴ではなく呼吸スペースとして読めます。

代表的なコンテンツでの測定された垂直フィル: 74〜79%、バンド 99〜124px。意図的に疎なケース(1 行の見出し、3 つの 1 行ポイント)は 68.8% で、バンドは 150px のキャップに達しています — バンドにハードキャップがあるため、これはその少量のテキストに対する算術上の最大値であり、さらに上げるとポイントがリストとして読めなくなるほど離れてしまいます。

見出しは文の大文字小文字の Barlow 700、行間 1.1、文字間隔 −0.5px。Cinzel は「WE ONE AVIATION」ワードマークにのみ残ります。文の大文字小文字はコードで強制されていません — 機械的に見出しを小文字にすると DGCA、ATPL、AAI が壊れるため — 代わりに headline フィールドの説明で指定されています。

各テンプレートはインラインのフラット SVG アクセントを持ちます: 幅広の斜め罫線(ニュース)、同心円のコンパスアーク(科目)、上昇するシェブロンスタック(キャリア)。ゴールド 7% で、テキストの背後で右下からはみ出します。これらはサムネイルサイズで構図に重みを与えるために存在し、テキストのコントラストに影響するにはあまりにも淡いです。

アクセントは .anchor-wrap 内にあり、overflow: hidden でキャンバスに固定されたボックスです。これがないと、絶対配置されたグラフィックが下端を越えて垂れ下がり、body.scrollHeight にカウントされ、オーバーフローガードが一定の 160px のページオーバーフローですべてのレンダリングを拒否します。

ロゴ

src/templates/assets/logo.png は提供されたロックアップです: 「WE ONE AVIATION」ワードマークの上にあるスター/航空機マーク。ヘッダーはそのワードマーク自体を Cinzel でレンダリングするため、scripts/prepare-logo.mjs はブランド名を 2 回印刷しないように、マークのみの logo-mark.png を導出します。非透明ピクセルの水平バンドを見つけて最も高いものを保持するため、ロゴを別の解像度で再エクスポートしても機能します。logo.png を置き換えた後:

npm run prepare-logo

ブラウザのライフサイクルとメモリ

1 つの Chromium がプロセスの存続期間中共有され、切断された場合のみ再起動されます。起動には約 1 秒と数百 MB かかり、投稿ごとに繰り返すにはあまりにも高コストです。

レンダリングメモリ。 Chromium は Node に加えて約 400 MB の常駐メモリを必要とします。無料インスタンスは 512 MB で、その下では OOM になります — デプロイは有用なログ行なしでレンダリング中に停止します。Starter プランを使用してください。 どうしても無料プランに留まる場合は、再起動を想定し、各再起動後の最初のレンダリングをコールドスタートとして扱ってください。

バイナリの取得元はホストによって異なります:

ホスト

ソース

CHROMIUM_EXECUTABLE_PATH 設定あり

そのパス、常に優先

Linux(Render)

@sparticuz/chromium。独自のバイナリを同梱しているため、ビルド時のブラウザダウンロードは不要

macOS / 開発

playwright-core がすでにキャッシュしているもの(npx playwright-core install chromium)

--single-process は意図的に使用されていません。 これはブラウザの再利用と互換性がありません: このフラグの下で BrowserContext を閉じるとブラウザ全体が破棄されるため、2 回目のレンダリングは "Target page, context or browser has been closed" で失敗します。このコードベースで測定: このフラグありでは 3 つのコンテキストのうち 1 つが生存、なしでは 3 つのうち 3 つが生存。再利用はトレードオフのより価値のある半分です。ホストが要求する場合は CHROMIUM_SINGLE_PROCESS=1 を設定して強制的に戻し、起動ごとに 1 レンダリングを想定してください。

エラー処理

コード

意味

bad_input

引数がバリデーションに失敗しました。

duplicate_topic

トピックはすでに存在します。設計どおりの動作です — 別のものを選んでください。

not_found

その history_id に対応する posts 行がありません。reserve_topic が呼び出されましたか?

db_error / storage_error

Supabase が拒否しました。details に Postgres コードが含まれます。

image_generation_failed / image_too_large

プロバイダーまたは sharp の問題。

image_url_unreachable

Meta がフェッチする URL が到達可能な JPEG ではありません。

caption_too_long

キャプション本文だけで 2200 文字を超えています。ハッシュタグは自動的にトリミングされますが、本文は決してトリミングされません。

meta_error

Graph API。details には code、error_subcode、type、fbtrace_id が変更なしで含まれます。

timeout

何かが予算を超えました(画像 60 秒、コンテナポーリング 60 秒、Graph 30 秒)。

config_error

必須の環境変数がありません。retryable: false。

Meta コード 190 と 200 は決して再試行されません。 190 は期限切れまたは無効なトークン、200 は権限の欠落です。どちらも人間の対応が必要で、再試行はレート制限を消費するだけで本当の原因を隠します。これらのエラーは retryable: false と、何をすべきかを示す needs_human メモとともに返されます。

すべてのツール呼び出しは tool.start と tool.ok/tool.error を期間付きでログに記録し、すべての Graph 呼び出しは graph.call をメソッド、エンドポイント、ステータス、経過ミリ秒付きでログに記録します — そのため Render のログビューアだけで実行を再構築できます。

トラブルシューティング

症状

原因

すべてのリクエストで 401

ヘッダーが欠落しているか、トークンが MCP_AUTH_TOKEN と一致しません。

/mcp で 500 config_error、/health は正常

サービスに MCP_AUTH_TOKEN が設定されていません。

image_url_unreachable

post-images バケットが公開されていないか、アップロードが静かに失敗しました。npm run smoke を実行してください。

IG コンテナが 60 秒間 IN_PROGRESS のまま

Meta が画像を取得できないか、処理が遅いです。まずブラウザで URL を確認してください。

meta_error コード 190

トークンが期限切れです。新しい長期有効な Page トークンを発行してください。check_token() は残り 7 日以下で警告します。

meta_error コード 200

スコープが不足しています — instagram_content_publish / pages_manage_posts を再付与してください。

duplicate_topic

バグではありません。トピックはすでに posts に存在します。

その日の最初の呼び出しがタイムアウトする

無料プランのコールドスタートです。まず /health にアクセスするか、starter に移行してください。

レンダリング中にワーカーがエラーなしで停止する

メモリ不足です。Chromium は約 400 MB 必要ですが、無料インスタンスは 512 MB です。starter に移行してください。

bad_input がフィールドを指定する

意図した動作です。指定されたフィールドを短縮して render_post を再度呼び出してください — 何もアップロードされていません。

Self-hosted fonts failed to load

src/templates/fonts/ が dist/ に到達しませんでした。npm run build を再実行してください。レンダリングは拒否され、フォールバックのセリフ体で提供されることはありません。

Target page, context or browser has been closed

CHROMIUM_SINGLE_PROCESS=1 が設定されています。このフラグは起動ごとに 1 回のレンダリングのみ許可します — 設定を解除してください。

Render でテンプレートファイルが見つからない

npm run build がスキップされたため、dist/templates/ が存在しません。

レイアウト

src/
  server.ts            Express, bearer auth, /health, POST /mcp
  config.ts            Lazy env resolution, constants
  log.ts               Timed stdout logging
  errors.ts            AppError / MetaError, the no-retry rule for 190 & 200
  supabase.ts          posts CRUD + storage upload
  meta.ts              Graph client, IG container flow, FB photos, debug_token
  image.ts             Chromium lifecycle, template render, overflow guard, sharp
  tools/
    register.ts        Timing, error envelope, content-block shaping
    get_past_topics.ts reserve_topic.ts render_post.ts
    publish_socials.ts mark_draft.ts check_token.ts
    index.ts
  templates/
    tokens.css base.css          design tokens + shared skeleton
    news.html subject.html career.html
    fonts/     barlow-400/600/700, cinzel-600 (woff2, self-hosted)
    assets/    logo.png (supplied), logo-mark.png (derived)
migrations/001_init.sql
scripts/
  smoke.ts           render all three, upload, prove both guards fire
  copy-templates.mjs build step: tsc emits only .ts, templates must reach dist/
  prepare-logo.mjs   derives logo-mark.png from logo.png
render.yaml

Related MCP Connectors

Related MCP Servers

  • A
    license
    A
    quality
    D
    maintenance
    MCP server to safely publish posts to multiple Facebook Pages via Meta Graph API, with built-in guardrails for brand voice, banned topics, image requirements, and anti-duplication.
    4
    MIT
  • A
    license
    B
    quality
    B
    maintenance
    Enables reading, publishing, commenting on, and analyzing Instagram Business/Creator accounts through the official Graph API, with multi-account support and an optional self-hosted publishing panel.
    29
    MIT