weone-daily-post
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ツール
ツール | 目的 |
| 新しい順の履歴、最大200行: |
|
|
| ブランド化された HTML テンプレートを正確なサイズの sRGB JPEG にレンダリングし、アップロードして、画像ブロックと公開 URL を返します。 |
| Instagram に公開し、次に Facebook に公開し、結果を記録します。 |
| シャドウモード: 公開せずに完成した投稿を |
| Meta トークンの有効期限までの日数と付与されたスコープ。 |
すべてのツールは JSON を返します。成功は {"ok": true, ...}、失敗は {"ok": false, "error": {code, message, retryable, details}} を含む MCP エラー結果です。呼び出し元に生のスタックトレースが投げられることはありません。
render_post
template は news、subject、career のいずれか — posts テーブルが使用するのと同じ3つのカテゴリです。
フィールド | 制限 | 備考 |
| 60文字 | Barlow 700、最大3行。文の大文字小文字(タイトルケースではありません)。 |
| 3〜4項目、各90文字 | Barlow 400、各項目に金色のマーカー |
| 32文字、任意 | 金色、CSS で大文字化、例: |
| 90文字、任意 | フッターバーの左側、例: |
これは2つのコンテンツブロックを返します: 画像ブロック(base64 JPEG)と、公開 URL、ファイル名、寸法、バイトサイズ、レンダリング時間を含むテキストブロックです。
アップロード前に2つのガードが実行され、どちらも問題のあるフィールドを指名します:
長さ制限 — Chromium に触れる前にチェックされる、安価な拒否です。
points[2] is 97 characters, limit is 90. Shorten it and retry.ページ内計測 — レイアウト後、すべてのテキストボックスは固定サイズのクリッピングボックスであり、その内容がボックスより縦または横に大きい場合、レンダリングはフィールド名とピクセル単位のオーバーフローとともに拒否されます。これは文字数では見えない問題、例えば長さは合法だが端からはみ出す分割不可能な80文字のトークンを捕捉します。
どちらのガードが発動しても何もアップロードされないため、拒否にかかるコストは1秒で、修正は常に「指名されたフィールドを短くする」ことです。
画像ブロックは文脈で文言を確認できるように返されますが、もはや正しさのチェックではありません。固定テンプレートは単語を誤綴したり、図をでっち上げたりすることはできません。最悪の合法入力(58文字の見出しと4つの90文字のポイント、最も広い合法な eyebrow と footer)は、3つのテンプレートすべてに収まることが検証されています。
publish_socials、ステップバイステップ
image_urlにHEADを送り、200とcontent-type: image/jpegを確認します。Meta はこの URL をサーバーサイドで取得するため、不正な URL は数時間後に不透明に失敗します。(HEADを拒否するストアには、1バイトの範囲GETが代わりに送信されます。)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 が何を嫌ったかを説明する唯一の場所だからです。Facebook —
POST {FB_PAGE_ID}/photosをurlとmessage付きで実行します。Instagram の結果に関係なく試行されます。記録 —
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
環境変数
変数 | 必須 | 説明 |
| はい |
|
| はい |
|
| はい | サービスロールキー。RLS をバイパスします — サーバーサイドのみ。コネクタ設定には絶対に置かないでください。 |
| いいえ(デフォルト | すべての呼び出しに使用される Graph API バージョン。 |
| 公開用 | Instagram ビジネスアカウント ID(数値、@ハンドルではありません)。 |
| 公開用 | その Instagram アカウントにリンクされた Facebook ページ ID。 |
| 公開用 |
|
| いいえ | Render がこれを設定します。デフォルト 10000。 |
| いいえ(デフォルト | このサイズを超えると、インラインの base64 プレビューがダウンスケールされます。 |
| いいえ | Chrome/Chromium バイナリへの明示的なパス。プラットフォームごとのデフォルトを上書きします。 |
| いいえ |
|
ローカル実行用に .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)を使用:
このリポジトリを GitHub にプッシュします。
Render ダッシュボード → New → Blueprint → リポジトリを選択します。
render.yamlを読み取ります: Node 20、npm ci && npm run build、npm start、/healthでのヘルスチェック。Render は
sync: falseのすべての変数を要求します。それらを貼り付けます。デプロイし、ログで
server.listening ... auth=configuredを確認します。auth=MISSINGはMCP_AUTH_TOKENが設定されなかったことを意味します。
手動:
新規 → Web Service → リポジトリを接続します。
ランタイム Node、ビルド
npm ci && npm run build、起動npm start。ヘルスチェックパス
/health。上記の表の環境変数に加えて、
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 startnpm 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 プランを使用してください。 どうしても無料プランに留まる場合は、再起動を想定し、各再起動後の最初のレンダリングをコールドスタートとして扱ってください。
バイナリの取得元はホストによって異なります:
ホスト | ソース |
| そのパス、常に優先 |
Linux(Render) |
|
macOS / 開発 |
|
--single-process は意図的に使用されていません。 これはブラウザの再利用と互換性がありません: このフラグの下で BrowserContext を閉じるとブラウザ全体が破棄されるため、2 回目のレンダリングは "Target page, context or browser has been closed" で失敗します。このコードベースで測定: このフラグありでは 3 つのコンテキストのうち 1 つが生存、なしでは 3 つのうち 3 つが生存。再利用はトレードオフのより価値のある半分です。ホストが要求する場合は CHROMIUM_SINGLE_PROCESS=1 を設定して強制的に戻し、起動ごとに 1 レンダリングを想定してください。
エラー処理
コード | 意味 |
| 引数がバリデーションに失敗しました。 |
| トピックはすでに存在します。設計どおりの動作です — 別のものを選んでください。 |
| その |
| Supabase が拒否しました。 |
| プロバイダーまたは sharp の問題。 |
| Meta がフェッチする URL が到達可能な JPEG ではありません。 |
| キャプション本文だけで 2200 文字を超えています。ハッシュタグは自動的にトリミングされますが、本文は決してトリミングされません。 |
| Graph API。 |
| 何かが予算を超えました(画像 60 秒、コンテナポーリング 60 秒、Graph 30 秒)。 |
| 必須の環境変数がありません。 |
Meta コード 190 と 200 は決して再試行されません。 190 は期限切れまたは無効なトークン、200 は権限の欠落です。どちらも人間の対応が必要で、再試行はレート制限を消費するだけで本当の原因を隠します。これらのエラーは retryable: false と、何をすべきかを示す needs_human メモとともに返されます。
すべてのツール呼び出しは tool.start と tool.ok/tool.error を期間付きでログに記録し、すべての Graph 呼び出しは graph.call をメソッド、エンドポイント、ステータス、経過ミリ秒付きでログに記録します — そのため Render のログビューアだけで実行を再構築できます。
トラブルシューティング
症状 | 原因 |
すべてのリクエストで | ヘッダーが欠落しているか、トークンが |
| サービスに |
|
|
IG コンテナが 60 秒間 | Meta が画像を取得できないか、処理が遅いです。まずブラウザで URL を確認してください。 |
| トークンが期限切れです。新しい長期有効な Page トークンを発行してください。 |
| スコープが不足しています — |
| バグではありません。トピックはすでに |
その日の最初の呼び出しがタイムアウトする | 無料プランのコールドスタートです。まず |
レンダリング中にワーカーがエラーなしで停止する | メモリ不足です。Chromium は約 400 MB 必要ですが、無料インスタンスは 512 MB です。 |
| 意図した動作です。指定されたフィールドを短縮して |
|
|
|
|
Render でテンプレートファイルが見つからない |
|
レイアウト
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.yamlThis server cannot be deployed
Maintenance
Related MCP Connectors
Create, schedule and publish Instagram and Facebook posts from Claude or ChatGPT.
- MarkyOAuthai.mymarky
Create, schedule, and publish on-brand social posts to Instagram, LinkedIn, TikTok, and more.
Multi-platform social media post creation, scheduling, and publishing.
131Create, review, publish and schedule Instagram images, carousels and Reels with AI assistants.
Related MCP Servers
- AlicenseAqualityDmaintenanceMCP 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.4MIT
- FlicenseNot gradedqualityBmaintenanceEnables publishing and managing organic Facebook Page and Instagram content directly through Meta's Graph API without paid third-party services.-
- AlicenseAqualityCmaintenanceEnables publishing to Facebook, Instagram, and YouTube through official APIs using your own OAuth credentials, with support for images, videos, Reels/Stories, and scheduled posts.6MIT
- AlicenseBqualityBmaintenanceEnables 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.29MIT