weone-daily-post
weone-daily-post — Remote-MCP-Server
Publishing-Backend für den täglichen Instagram- und Facebook-Post von We One Aviation. Es ist ein zustandsloser Streamable HTTP MCP-Server: Claude übernimmt das Denken (Themenwahl, Formulierung, Bildunterschrift), dieser Dienst übernimmt die Nebeneffekte (Verlauf, Bildrendering, Speicherung, Graph API). Bilder werden gesetzt, nicht generiert: Der Text auf dem Poster ist exakt der gelieferte Text.
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 photosTools
Tool | Zweck |
| Verlauf, neueste zuerst, max. 200 Zeilen: |
| Fügt |
| Rendert eine gebrandete HTML-Vorlage zu einem JPEG in exakter Größe (sRGB), lädt es hoch und gibt einen Bildblock plus die öffentliche URL zurück. |
| Veröffentlicht auf Instagram und dann Facebook, protokolliert das Ergebnis. |
| Schattenmodus: Den fertigen Post als |
| Tage bis zum Ablauf des Meta-Tokens + gewährte Berechtigungen. |
Jedes Tool gibt JSON zurück. Erfolg ist {"ok": true, ...}; Fehler sind ein MCP-Fehler
Ergebnis mit {"ok": false, "error": {code, message, retryable, details}}.
Nichts wirft einen rohen Stacktrace an den Aufrufer.
render_post
template ist eines von news, subject oder career — dieselben drei Kategorien,
die auch die posts-Tabelle verwendet.
Feld | Limit | Hinweise |
| 60 Zeichen | Barlow 700, bis zu 3 Zeilen. Satzfall, nicht Titelcase. |
| 3–4 Punkte, je 90 Zeichen | Barlow 400, je ein goldener Marker |
| 32 Zeichen, optional | Gold, per CSS in Großbuchstaben, z. B. |
| 90 Zeichen, optional | Linke Seite der Fußzeile, z. B. |
Es gibt zwei Inhaltsblöcke zurück: einen Bildblock (Base64-JPEG) und einen Text Block mit der öffentlichen URL, Dateiname, Abmessungen, Bytegröße und Renderzeit.
Zwei Wächter laufen, bevor etwas hochgeladen wird, und beide nennen das fehlerhafte Feld:
Längenlimits, geprüft bevor Chromium berührt wird — die kostengünstige Ablehnung.
points[2] is 97 characters, limit is 90. Shorten it and retry.Messung im Layout, nach dem Layout — jede Textbox ist eine feste Beschneidungsbox, und wenn ihr Inhalt höher oder breiter als die Box ist, wird das Rendern mit dem Feldnamen und dem Überlauf in Pixeln abgelehnt. Das fängt ab, was eine Zeichenanzahl nicht sehen kann, z. B. ein unteilbares 80-Zeichen-Token, das in der Länge legal ist, aber über den Rand läuft.
Nichts wird hochgeladen, wenn einer der Wächter auslöst, also kostet eine Ablehnung eine Sekunde und die Lösung ist immer „das genannte Feld kürzen“.
Der Bildblock kommt trotzdem zurück, damit du die Formulierung im Kontext lesen kannst, aber er ist keine Korrektheitsprüfung mehr: Eine feste Vorlage kann kein Wort falsch schreiben oder ein Diagramm erfinden. Der ungünstigste legale Eingabefall (58-Zeichen-Überschrift plus vier 90-Zeichen-Punkte, breitestes legales Eyebrow und Footer) wurde verifiziert, um in alle drei Vorlagen zu passen.
publish_socials, Schritt für Schritt
HEADauf dieimage_urlund200+content-type: image/jpegbestätigen. Meta ruft diese URL serverseitig ab, und eine schlechte URL schlägt dort stundenlang undurchsichtig fehl. (Ein Speicher, derHEADablehnt, bekommt stattdessen einen 1-Byte-Bereichs-GET.)Instagram —
full_caption = caption + "\n\n" + hashtags.join(' '), begrenzt auf 2200 Zeichen. Nur Hashtags werden entfernt, vom Ende; der Bildunterschriftstext wird nie abgeschnitten. Wenn der Text allein 2200 überschreitet, schlägt der Aufruf mitcaption_too_longfehl, bevor etwas gepostet wird.POST {IG_USER_ID}/media→GET {container}?fields=status_code,statuseinmal pro Sekunde für bis zu 60s pollen → nur beiFINISHEDveröffentlichen. BeiERRORwird diestatus-Zeichenfolge wörtlich zurückgegeben, denn sie ist die einzige Stelle, an der Meta erklärt, was es nicht mochte.Facebook —
POST {FB_PAGE_ID}/photosmiturlundmessage. Wird unabhängig vom Instagram-Ergebnis versucht.Aufzeichnen — die
posts-Zeile erhältig_post_id,fb_post_id,image_urlundstatus=published(beide),partial(eines) oderfailed(keines).
Gibt {ig_post_id, fb_post_id, status, errors: [...]} zurück. Ein Fehler auf einer Plattform
wird nie verschluckt: Er erscheint in errors[] mit der Plattform, der
Stufe, bei der er auftrat, und Metas eigenem code / error_subcode / message.
Related MCP server: Social Analytics MCP Server
Umgebungsvariablen
Variable | Erforderlich | Was es ist |
| ja | Gemeinsames Geheimnis für |
| ja |
|
| ja | Service-Rollen-Schlüssel. Umgeht RLS — nur serverseitig. Nie in die Connector-Konfiguration legen. |
| nein (Standard | Graph-API-Version, die für jeden Aufruf verwendet wird. |
| für Veröffentlichung | Instagram-Business-Konto-ID (eine Zahl, nicht der @Handle). |
| für Veröffentlichung | Facebook-Seiten-ID, die mit diesem Instagram-Konto verknüpft ist. |
| für Veröffentlichung | Langlebiges Seiten-Zugriffstoken mit |
| nein | Render setzt dies. Standard 10000. |
| nein (Standard | Größe, ab der die Inline-Base64-Vorschau herunterskaliert wird. |
| nein | Expliziter Pfad zu einer Chrome/Chromium-Binärdatei. Überschreibt den plattformspezifischen Standard. |
| nein | Auf |
Kopiere .env.example nach .env für lokale Läufe. .env ist gitignored — lass es so.
Einrichtung
1. Supabase
Führe migrations/001_init.sql im SQL-Editor aus (oder supabase db push). Es ist
idempotent und erstellt:
die
posts-Tabelle mit den Check-Constraints und dem eindeutigen Index auftopic— dieser Index ist die Wiederholungssperre, also muss eine doppelte Reservierung erwartungsgemäß fehlschlagen,created_at desc- undstatus-Indizes,RLS auf
postsohne Richtlinien (nur der Service-Schlüssel kommt rein),den öffentlichen
post-images-Speicher-Bucket plus seine öffentliche Lese-Richtlinie. Öffentliches Lesen ist Pflicht: Meta ruft das JPEG selbst ab und kann keine Anmeldedaten vorweisen.
2. Meta
Du brauchst ein Instagram Business- oder Creator-Konto, das mit einer Facebook-Seite verknüpft ist,
und ein langlebiges Seiten-Token mit den oben aufgeführten Berechtigungen. Bestätige es mit
check_token() vor dem ersten Lauf — ein abgelaufenes Token ist die häufigste Ursache für einen fehlgeschlagenen Morgen.
3. Auf Render bereitstellen
Mit render.yaml (Blueprint):
Dieses Repo auf GitHub pushen.
Render-Dashboard → Neu → Blueprint → das Repo auswählen. Es liest
render.yaml: Node 20,npm ci && npm run build,npm start, Health-Check auf/health.Render fragt nach jeder Variable mit
sync: false. Füge sie ein.Bereitstellen, dann in den Logs nach
server.listening ... auth=configuredsuchen.auth=MISSINGbedeutet, dassMCP_AUTH_TOKENnicht gesetzt wurde.
Manuell:
New → Web Service → das Repository verbinden.
Runtime Node, Build
npm ci && npm run build, Startnpm start.Health-Check-Pfad
/health.Die Umgebungsvariablen aus der obigen Tabelle hinzufügen, plus
NODE_VERSION=20.
Überprüfen:
curl https://<your-service>.onrender.com/health
# {"status":"ok","server":{...},"tools":[...six...],"uptime_s":3}Den Starter-Plan verwenden, nicht den kostenlosen. Chromium benötigt etwa 400 MB Arbeitsspeicher zusätzlich zu Node, und die kostenlose Instanz hat 512 MB – es wird mitten im Rendering ein OOM auftreten, und der Fehler erscheint als toter Worker statt als nützliche Logzeile. Der kostenlose Plan schläft außerdem nach Inaktivität ein, sodass der erste Tool-Aufruf des Tages zusätzlich einen 30–60 Sekunden dauernden Kaltstart bezahlt. render.yaml setzt aus beiden Gründen starter.
Beim Build ist kein Browser-Download nötig: @sparticuz/chromium bringt sein eigenes Binary als Abhängigkeit mit, also ist npm ci && npm run build der gesamte Build. Dieser Build-Schritt kopiert auch src/templates/ nach dist/ – tsc emittiert nur .ts, also startet der Server ohne diesen Schritt zwar einwandfrei, schlägt aber beim ersten Rendern mit einer fehlenden Template-Datei fehl.
4. Mit Claude verbinden
Der Endpunkt ist:
https://<your-service>.onrender.com/mcpmit dem Header:
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 (projektbezogen, ohne Token eingecheckt):
{
"mcpServers": {
"weone-social": {
"type": "http",
"url": "https://<your-service>.onrender.com/mcp",
"headers": { "Authorization": "Bearer ${MCP_AUTH_TOKEN}" }
}
}
}Im Dialog für den benutzerdefinierten Connector von Claude Desktop/Web dieselbe /mcp-URL einfügen und das Bearer-Token in das Feld für den Request-Header setzen. Die Authentifizierung ist bewusst nur über den Header möglich – das Token wird nie als Query-Parameter akzeptiert, weil URLs in Proxy-Logs und Browserverläufen landen.
Die Anweisungen für den täglichen Lauf (Markenrichtlinien, verbotene Behauptungen, Kategorienrotation, Bildspezifikation, QA-Checkliste) liegen im weone-daily-post-Skill, nicht in diesem Server. Dieser Dienst enthält bewusst keine redaktionelle Richtlinie.
Lokale Entwicklung
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 neededRendert je eine Vorlage von jedem Typ, schreibt alle drei JPEGs nach ./out, lädt sie hoch, prüft jede öffentliche URL per HEAD und gibt die drei Links aus. Danach beweist es, dass beide Guards weiterhin greifen. Es berührt keinen Meta-Endpunkt, ist also gegen Produktionsanmeldedaten sicher. Für den Upload werden nur SUPABASE_URL und SUPABASE_SERVICE_KEY benötigt; --no-upload benötigt nichts.
Lokale Dateien werden vor dem Upload geschrieben, sodass ein Supabase-Fehler trotzdem etwas zum Ansehen hinterlässt.
Rendering-Pipeline
Ein Headless-Chromium lädt src/templates/{template}.html über file://, die Werte werden in das DOM geschrieben und die Seite wird gescreenshottet. Dieselbe Eingabe erzeugt immer dieselben Pixel.
Vorlagen liegen in
src/templates/.tokens.cssenthält alle Farben;base.cssenthält das Grundgerüst, das alle drei gemeinsam haben. Eine Vorlagendatei unterscheidet sich von ihren Geschwistern nur in der Eyebrow-Behandlung und dem Punkt-Marker (News: goldene Regel, Thema: nummerierte goldene Kreise, Karriere: goldene Chevrons).Schriftarten sind selbst gehostet in
src/templates/fonts/(Barlow 400/600/700 für alles, Cinzel 600 nur für das Wortzeichen, lateinische Teilmengen, OFL). Beim Rendern wird nichts abgerufen – ein Netzwerkaufruf würde die Ausgabe nicht deterministisch machen und auf Render stillschweigend fehlschlagen, mit Fallback auf eine System-Serife. Der Renderer wartet aufdocument.fonts.readyund prüft dann, dass beide Schriftschnitte tatsächlich geladen wurden, statt einen Fallback zu screenshotten.Kein Benutzertext wird jemals in Markup konkateniert. Werte gehen über
textContentundcreateElementhinein, also gibt es kein Escaping, das falsch sein könnte: Ein<script>in einer Überschrift landet auf dem Poster als die wörtlichen Zeichen.Viewport 1080×1350 bei
deviceScaleFactor: 2, also ist der Screenshot 2160×2700 und wird herunterskaliert – Textkanten bleiben sauber.sharp:
resize(1080, 1350, {fit:'cover'})→toColorspace('srgb')→jpeg({quality: 90, chromaSubsampling: '4:4:4'}), Metadaten entfernt. 4:4:4 ist keine Dekoration – 4:2:0 verschmiert farbige Textkanten, und diese Poster sind Text.Prüft, dass das kodierte JPEG unter 8 MB liegt und die dekodierten Abmessungen wirklich die angeforderten sind.
Upload als
{yyyy-mm-dd}-{slug}-{6 hex}.jpg(UTC-Datum). Jedes Rendering bekommt seinen eigenen Schlüssel und nichts wird jemals überschrieben –upsert: false. Ein erneutes Rendern eines Themas kann das Bild unter einem bereits veröffentlichten Beitrag mit der vorherigen URL nicht ändern.cacheControlist aus demselben Grund 60s: Ein schlechtes Objekt bleibt innerhalb einer Minute korrigierbar, statt ein Jahr lang in einem CDN festzustecken. Meta ruft die URL einmal serverseitig kurz nach dem Upload ab, also braucht nichts den langen Cache. Objekte sammeln sich an; Speicher ist viel billiger als ein veraltetes Bild auf einem Live-Beitrag.
Layout-Verhalten
Die Typografie skaliert mit der Punktanzahl. Drei Punkte bekommen eine 68px-Überschrift und 36px-Textkörper; vier bekommen 60px und 32px. Das wird in CSS mit :has() gemacht, also liegt die Layout-Entscheidung vollständig in der Vorlage und der Renderer weiß weder davon noch kümmert es ihn. Der Overflow-Guard läuft nach der Skalierung, also wird das skalierte Ergebnis gemessen.
Der Inhaltsblock ist vertikal zentriert zwischen der Kopfzeilen-Regel und der Fußzeilen-Leiste. Feste Abstände allein können kein Füllziel halten, weil die Textmenge variiert, also teilen sich drei elastische Elemente den verbleibenden Raum: ein Band oben, ein Band unten und der Abstand unter der Überschrift. Die Bänder sind hart auf 150px begrenzt, was „kein großer leerer Rand“ erzwingt; sobald sie das Limit erreichen, fließt der Überschuss in den Abstand unter der Überschrift, wo er wie Atemraum wirkt statt wie ein Loch.
Gemessene vertikale Füllung bei repräsentativem Inhalt: 74–79 %, Bänder 99–124px. Ein bewusst spärlicher Fall (einzeilige Überschrift, drei einzeilige Punkte) liegt bei 68,8 % mit den Bändern an ihrem 150px-Limit – mit einer harten Obergrenze für die Bänder ist das das arithmetische Maximum für so wenig Text, und es weiter anzuheben würde bedeuten, die Punkte so weit zu verteilen, dass sie nicht mehr wie eine Liste lesen.
Überschriften sind Barlow 700 im Satzfall, Zeilenhöhe 1.1, Buchstabenabstand −0.5px. Cinzel überlebt nur im „WE ONE AVIATION“-Wortzeichen. Der Satzfall wird nicht im Code erzwungen – mechanisches Kleinschreiben einer Überschrift würde DGCA, ATPL und AAI zerstören – also wird er stattdessen in der Feldbeschreibung von headline spezifiziert.
Jede Vorlage trägt ein flaches SVG-Akzent inline: eine breite diagonale Regel (News), konzentrische Kompassbögen (Thema), ein aufsteigender Chevron-Stapel (Karriere). Gold bei 7 %, läuft unten rechts hinter dem Text aus. Sie existieren, um der Komposition in Thumbnail-Größe Gewicht zu geben, und sind viel zu dezent, um den Textkontrast zu beeinträchtigen.
Das Akzent sitzt in
.anchor-wrap, einer Box, die mitoverflow: hiddenan der Leinwand fixiert ist. Ohne sie hängt die absolut positionierte Grafik über die untere Kante hinaus, zählt zubody.scrollHeight, und der Overflow-Guard lehnt jedes Rendering mit einem konstanten 160px-Seitenüberlauf ab.
Das Logo
src/templates/assets/logo.png ist das gelieferte Lockup: das Stern-/Flugzeugzeichen über einem „WE ONE AVIATION“-Wortzeichen. Der Header rendert dieses Wortzeichen selbst in Cinzel, also leitet scripts/prepare-logo.mjs logo-mark.png ab – nur das Zeichen – um den Markennamen nicht zweimal zu drucken. Es findet die horizontalen Bänder nicht transparenter Pixel und behält das höchste, sodass ein erneuter Export des Logos in einer anderen Auflösung weiterhin funktioniert. Nach dem Ersetzen von logo.png:
npm run prepare-logoBrowser-Lebenszyklus und Speicher
Ein Chromium wird für die Lebensdauer des Prozesses geteilt und nur neu gestartet, wenn es die Verbindung verliert. Das Starten kostet etwa eine Sekunde und ein paar hundert MB, viel zu viel, um es pro Beitrag zu wiederholen.
Render-Speicher. Chromium benötigt etwa 400 MB Arbeitsspeicher zusätzlich zu Node. Die kostenlose Instanz hat 512 MB und wird darunter einen OOM erleiden – der Deploy stirbt mitten im Rendering ohne nützliche Logzeile. Den Starter-Plan verwenden. Wenn du auf dem kostenlosen Plan bleiben musst, erwarte Neustarts und behandle das erste Rendering nach jedem als Kaltstart.
Woher das Binary kommt, hängt vom Host ab:
Host | Quelle |
| dieser Pfad, gewinnt immer |
Linux (Render) |
|
macOS / Dev | was auch immer |
--single-process wird bewusst nicht verwendet. Es ist inkompatibel mit der Wiederverwendung eines Browsers: Das Schließen eines BrowserContext unter diesem Flag reißt den gesamten Browser mit, also schlägt das zweite Rendering mit „Target page, context or browser has been closed“ fehl. Gemessen an dieser Codebasis: 1 von 3 Kontexten überlebt damit, 3 von 3 ohne. Wiederverwendung ist die wertvollere Hälfte des Tauschs. Setze CHROMIUM_SINGLE_PROCESS=1, um es wieder zu erzwingen, falls ein Host es jemals verlangt, und erwarte ein Rendering pro Start.
Fehlerbehandlung
Code | Bedeutung |
| Argumente haben die Validierung nicht bestanden. |
| Das Thema existiert bereits. Funktioniert wie vorgesehen – ein anderes wählen. |
| Keine |
| Supabase hat Nein gesagt. |
| Provider- oder sharp-Problem. |
| Die URL, die Meta abrufen würde, ist kein erreichbares JPEG. |
| Der Caption-Text allein überschreitet 2200 Zeichen. Hashtags werden automatisch gekürzt; der Text selbst nie. |
| Graph API. |
| Etwas hat sein Budget überschritten (Bild 60s, Container-Poll 60s, Graph 30s). |
| Eine erforderliche Umgebungsvariable fehlt. |
Meta-Codes 190 und 200 werden nie erneut versucht. 190 ist ein abgelaufenes oder ungültiges Token, 200 ist eine fehlende Berechtigung; beide brauchen einen Menschen, und ein erneuter Versuch verbrennt nur das Rate-Limit, während die eigentliche Ursache verborgen bleibt. Diese Fehler kommen mit retryable: false und einer needs_human-Notiz zurück, die sagt, was zu tun ist.
Jeder Tool-Aufruf protokolliert tool.start und tool.ok/tool.error mit einer Dauer, und jeder Graph-Aufruf protokolliert graph.call mit Methode, Endpunkt, Status und verstrichenen Millisekunden – also reicht der Log-Viewer von Render aus, um einen Lauf zu rekonstruieren.
Fehlerbehebung
Symptom | Ursache |
| Header fehlt, oder der Token stimmt nicht mit |
|
|
| Der |
IG-Container hängt 60s lang bei | Meta kann das Bild nicht abrufen oder ist langsam. Prüfe die URL zuerst in einem Browser. |
| Token abgelaufen. Erstelle einen neuen langlebigen Page-Token. |
| Scope fehlt — erteile |
| Kein Bug. Das Thema ist bereits in |
Erster Aufruf des Tages läuft in einen Timeout | Kaltstart im Free-Plan. Rufe zuerst |
Worker stirbt während des Renderings, kein Fehler | Nicht genügend Arbeitsspeicher. Chromium benötigt ~400 MB; die kostenlose Instanz hat 512 MB. Wechsle zu |
| Funktioniert wie vorgesehen. Kürze das genannte Feld und rufe |
|
|
|
|
Vorlagendatei auf Render nicht gefunden |
|
Layout
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 installed
Maintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Related MCP Servers
- AlicenseAqualityCmaintenanceEnables AI assistants to manage Instagram and Threads accounts — publish content, handle comments, view insights, search hashtags, and manage DMs through the Meta Graph API.594610MIT
- AlicenseAqualityDmaintenanceEnables Instagram and Facebook analytics via the Meta Graph API, offering account insights, media analytics, and post engagement data.196MIT
- 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 gradedqualityCmaintenanceMCP server for automatic Instagram publishing — single image, carousel and Reels — via the official Instagram Graph API.40
Related MCP Connectors
Create, schedule and publish social posts to TikTok, Instagram, Facebook and YouTube.
Schedule and publish social posts to 11 platforms with media, campaigns, analytics and AI captions
Boost posts and launch community growth campaigns from your AI assistant. OAuth, credit-billed.
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/Weone404/mcp-content-cron'
If you have feedback or need assistance with the MCP directory API, please join our Discord server