TL;DR — Was du nach diesem Artikel hast
- 📺 Live-Demo dazu: das Dashboard in Aktion (15 Min) — Video unten am Ende.
- Eine eigene MCP-konforme Content-Pipeline mit 7 Steps: von Keyword-Idee zu live-deployedem Artikel.
- Einen lokalen Node.js-API-Server (Zero-Dependency), der die Pipeline orchestriert.
- Ein Astro-Frontend mit Dashboard, das Steps anstößt und Live-Logs zeigt.
- Auto-Deploy auf Cloudflare Pages über
git push.Voraussetzungen: Node.js 22+, Anthropic API-Key, Git-Remote. Kein Backend-Hosting nötig — alles läuft lokal. Zeitaufwand: 2–3 Stunden, wenn du den Code 1:1 übernimmst. Schwierigkeit: Builder mit TypeScript-Grundkenntnissen.
Du chattest mit Claude Desktop. Du hast eine Idee für einen neuen Artikel. Statt jetzt ins Notion zu wechseln, Recherche zu machen, im Editor zu schreiben, Bilder zu basteln, im Terminal zu pushen — tippst du einen Befehl und drückst Enter. Sieben Steps später ist der Artikel live.
Genau das löst die Pipeline, die ich dir hier zeige. Im Kern steht ein MCP-Server — das Model Context Protocol von Anthropic, mit dem du Claude saubere Verbindungen zu eigenen Daten und Tools gibst. Drumherum: ein lokaler API-Server, ein Astro-Frontend, ein Step-System, das du beliebig erweitern kannst.
Warum das überhaupt nötig ist: der manuelle Content-Workflow eines Solo-Builders ist ein Flickenteppich. Recherche in einem Tab, Notion daneben für die Keyword-DB, ein Editor für den Text, ein anderes Fenster für SEO-Tools, ein drittes für Bild-Generierung, am Ende der Terminal für git push. Jeder dieser Sprünge kostet zwei Minuten Kontext-Wiederaufbau. Bei einem Artikel pro Woche wird das zur Hauptarbeit — nicht das Schreiben selbst, sondern das Tool-Hopping drumherum.
Was sich ändert, wenn die Pipeline steht: Du tippst einen Titel, klickst durch sieben Buttons, der Artikel landet veröffentlicht im Repo. Dazwischen liegen ungefähr 5 Minuten echte Wartezeit (zwei lange Claude-Calls für Discovery und Schreiben), in denen du was anderes machen kannst — die Pipeline ist asynchron und schreibt ihren Fortschritt live in die UI.
Für wen das hier ist: Solo-Selbstständige und Indie-Hacker mit TypeScript-Grundkenntnissen. Du musst kein Backend-Profi sein — der HTTP-Server hier ist 150 Zeilen plain Node und hat keine externen Dependencies außer dem Anthropic-SDK. Du musst auch kein Astro-Profi sein — wir nutzen das Framework als statische Page-Engine plus ein bisschen Client-JS. Was du brauchst: Du musst dich trauen, ein paar Files anzulegen, ein paar Pakete zu installieren und einen API-Key in eine .env zu schreiben.
Was hier nicht drin ist, damit du nicht enttäuscht bist: kein fertiges Hosting für die Pipeline (sie läuft lokal — by design, weil Cloudflare Functions keine Child-Prozesse spawnen können), keine User-Auth (es ist dein persönlicher Workflow, niemand sonst kommt ran), keine Bildgenerierung über Replicate fertig verkabelt (zeige ich als Stub, der Pattern ist klar, eigene Anbindung ist Sache von 30 Minuten extra).
Das Endergebnis ist kein Spielzeug — es ist genau der Server, der diesen Artikel hier auf kiforge.de geschrieben hat. Du wirst es 1:1 nachbauen können. Quelle: dieses Repo.
Was wir am Ende haben
Ein Dashboard unter http://localhost:4321/dashboard/, das so aussieht: links eine Liste deiner Pipeline-Runs, pro Run sieben Step-Cards (Discovery → Research → Schreiben → Fakten-Check → SEO-Check → Bild → Veröffentlichen), rechts der aktuelle Output (Keyword-Vorschläge, Artikel-Vorschau, SEO-Checkliste) plus ein Live-Log-Terminal. Klick auf „Ausführen” startet einen Step, Claude denkt, das Frontend pollt live, am Ende landet ein MDX im Repo und ein git push deployt automatisch.
Architektur in einem Bild:
┌────────────────────────┐
│ Astro-Frontend :4321 │ ← du hier (Browser)
│ /dashboard/... │
└───────────┬────────────┘
│ fetch (CORS)
▼
┌────────────────────────┐
│ Node-HTTP-Server :4322 │ ← Pipeline-Orchestrator
│ scripts/server.mjs │
└───────────┬────────────┘
│
┌───────────┴───────────┐
▼ ▼
┌─────────┐ ┌──────────┐
│ Claude │ │ .runs/ │
│ API │ │ <id>.json│
└─────────┘ └────┬─────┘
│ MDX schreiben
▼
┌─────────────┐
│ src/content │ → git push → Cloudflare
└─────────────┘
Drei Sachen sind hier wichtig: die Trennung Frontend/Server (statisch deploybar bleibt statisch deploybar, der Pipeline-Server ist Dev-only), das State-Modell (eine JSON pro Run), und das Step-Pattern (jeder Step ist eine eigene Datei — Erweiterung = neue Datei).
Was ist MCP überhaupt?
Kurze Klärung, dann zum Code. MCP ist ein offener Standard von Anthropic, der definiert, wie ein LLM-Client (Claude Desktop, Cursor, Claude Code) mit einem Server spricht, den du selbst betreibst. Drei Bausteine:
- Tools — Funktionen, die der Client aufrufen kann (z.B.
analyze_keyword) - Resources — Daten, die der Client lesen kann
- Prompts — wiederverwendbare Prompt-Vorlagen
In dieser Pipeline benutzen wir MCP nicht direkt — wir bauen einen MCP-äquivalenten Workflow: ein eigenständiger HTTP-Server, der die Claude-API als Backend nutzt und den Workflow für uns orchestriert (statt für Claude). Wer später echtes MCP draufsetzen will (damit Claude Desktop die Pipeline selbst triggert), tauscht den HTTP-Layer gegen das @modelcontextprotocol/sdk — die Step-Logik bleibt identisch.
[VERIFY: Stand der MCP-SDK-Reife für TS-Server in Major-Version 1.x]
Voraussetzungen
- Node.js 22+ (
node --versionmussv22.xausgeben) - Anthropic API-Key unter console.anthropic.com/settings/keys — kostet wenige Cent pro Artikel
- Git-Remote auf GitHub oder GitLab — für den Auto-Deploy
- Editor — Cursor oder Claude Code empfohlen (Cursor-vs-Claude-Code-Vergleich)
Optional, kommt später dran:
- Cloudflare Pages Account (für den Auto-Deploy in Production)
- Replicate / OpenAI Images API-Key (wenn du echte Bild-Generierung statt Stub willst)
Schritt 1 — Astro-Projekt aufsetzen
npm create astro@latest dein-projekt -- --template blog --typescript strict --install --git --yes
cd dein-projekt
npx astro add tailwind --yes
npx astro add mdx --yes
npx astro add sitemap --yes
npm install -D @tailwindcss/typography
npm install @anthropic-ai/sdk
Dann astro.config.mjs aufräumen. Die wichtigen Bits:
import { defineConfig } from "astro/config";
import mdx from "@astrojs/mdx";
import sitemap from "@astrojs/sitemap";
import tailwindcss from "@tailwindcss/vite";
export default defineConfig({
site: "https://deine-domain.de",
output: "static",
trailingSlash: "ignore",
build: { format: "directory" },
integrations: [
mdx(),
sitemap({
filter: (page) => !page.includes("/dashboard"),
}),
],
vite: { plugins: [tailwindcss()] },
});
/dashboard ist aus der Sitemap raus, weil das ein lokales Dev-Tool ist und nichts in Google verloren hat.
Schritt 2 — Content Collection einrichten
src/content.config.ts definiert das Schema, gegen das jeder MDX-Artikel validiert wird. Bricht der Build, falls Frontmatter fehlt — Sicherheitsnetz gegen versehentlich kaputten Deploy.
import { defineCollection, z } from "astro:content";
import { glob } from "astro/loaders";
const articleSchema = z.object({
title: z.string(),
description: z.string(),
pubDate: z.coerce.date(),
updatedDate: z.coerce.date().optional(),
keywords: z.array(z.string()).default([]),
draft: z.boolean().default(false),
});
const agents = defineCollection({
loader: glob({ pattern: "**/[^_]*.{md,mdx}", base: "./src/content/agents" }),
schema: articleSchema,
});
export const collections = { agents };
Drei Säulen kannst du jederzeit ergänzen — bei kiforge sind es agents, lokal, anleitungen. Pattern [^_]* excludiert Files, die mit _ beginnen (bspw. _README.md).
Schritt 3 — Claude-Wrapper bauen
Eigene Datei scripts/lib/anthropic.mjs. Drei Funktionen, die später von den Steps benutzt werden:
import Anthropic from "@anthropic-ai/sdk";
const KIFORGE_VOICE = `
Schreib im Stil deines Projekts:
- Du-Form durchgehend
- Eigene Erfahrung sichtbar machen
- Code aus echten Projekten, mit Sprach-Tag
- Verboten: revolutionär, game-changing, krass, magisch
- Schmiede-Sprache nur als seltene Würze
`;
export function getClient() {
const key = process.env.ANTHROPIC_API_KEY;
if (!key) throw new Error("ANTHROPIC_API_KEY fehlt.");
return new Anthropic({ apiKey: key });
}
export async function generateArticleBody({ topic, pillar, keywords, targetWords = 1800 }) {
const client = getClient();
const prompt = `Schreibe einen deutschen Artikel.
Thema: ${topic}
Säulen-Fokus: ${pillar.angle}
SEO-Keywords: ${keywords.join(", ")}
${KIFORGE_VOICE}
Länge: ca. ${targetWords} Wörter.
WICHTIG: Nur Markdown-Body, kein Frontmatter. Setze [CHRIS-NOTIZ:…]-Platzhalter.`;
const response = await client.messages.create({
model: "claude-sonnet-4-5",
max_tokens: 8000,
messages: [{ role: "user", content: prompt }],
});
return response.content
.filter((b) => b.type === "text")
.map((b) => b.text)
.join("");
}
export async function generateKeywordSuggestions({ topic, pillar }) {
const client = getClient();
const prompt = `Schlag 5 deutsche Keyword-Phrasen vor zum Thema "${topic}".
Gib NUR ein JSON-Array zurück: [{"keyword","volume","difficulty","pillar","tags","description","similar"}]`;
const response = await client.messages.create({
model: "claude-sonnet-4-5",
max_tokens: 2000,
messages: [{ role: "user", content: prompt }],
});
const raw = response.content[0].text.trim().replace(/^```(?:json)?\n?/, "").replace(/\n?```$/, "");
return JSON.parse(raw);
}
Den vollen KIFORGE_VOICE-Block kannst du sehr ausgeprägt machen — er steckt in jedem Generate-Aufruf. Bei mir steht da auch eine Verbots-Liste mit allen Marketing-Wörtern, die ich nicht in meinen Texten haben will. Spart später Editor-Zeit.
Schritt 4 — Run-State + Step-Registry
Hier die Datenseite. Pro Run ein JSON-File unter .runs/<id>.json. Keine Datenbank, kein ORM, einfach Files.
scripts/lib/runs.mjs — die wichtigsten Exports:
import fs from "fs";
import path from "path";
import crypto from "crypto";
const RUNS_DIR = path.resolve(process.cwd(), ".runs");
export const DEFAULT_STEPS = [
{ num: 1, key: "discovery", title: "Discovery", description: "Claude schlägt Keyword-Lücken vor." },
{ num: 2, key: "research", title: "Research", description: "Web-Search & Briefing." },
{ num: 3, key: "write", title: "Schreiben", description: "Artikel-MDX nach Voice und Schema." },
{ num: 4, key: "fact-check", title: "Fakten-Check", description: "Aussagen vs. Quellen-Whitelist." },
{ num: 5, key: "seo", title: "SEO-Check", description: "Heuristik (Title/Meta/H2/FAQ)." },
{ num: 6, key: "image", title: "Bild generieren", description: "Hero-Image im Brand-Style." },
{ num: 7, key: "publish", title: "Veröffentlichen", description: "draft:false + git commit + push." },
];
export function createRun({ pillar, title, slug }) {
const id = crypto.randomBytes(4).toString("hex");
const run = {
id, title, slug, pillar,
currentStep: "discovery",
status: "draft",
cost: "$0.00",
startedAt: new Date().toISOString(),
steps: DEFAULT_STEPS.map((s, i) => ({
...s, status: i === 0 ? "active" : "pending", hasOutput: false,
})),
artifacts: {},
logs: [{ level: "info", text: `Run angelegt: ${title}` }],
};
saveRun(run);
return run;
}
export function saveRun(run) {
if (!fs.existsSync(RUNS_DIR)) fs.mkdirSync(RUNS_DIR, { recursive: true });
fs.writeFileSync(path.join(RUNS_DIR, `${run.id}.json`), JSON.stringify(run, null, 2));
}
export function loadRun(id) {
const p = path.join(RUNS_DIR, `${id}.json`);
return fs.existsSync(p) ? JSON.parse(fs.readFileSync(p, "utf8")) : null;
}
export function appendLog(run, level, text) {
run.logs.push({ level, text });
}
export function setStepStatus(run, stepKey, status, hasOutput) {
const step = run.steps.find((s) => s.key === stepKey);
step.status = status;
if (hasOutput !== undefined) step.hasOutput = hasOutput;
if (status === "active") run.currentStep = stepKey;
}
Dazu eine Step-Registry, die alle Step-Module dynamisch lädt. scripts/lib/steps/index.mjs:
import discovery from "./discovery.mjs";
import research from "./research.mjs";
import write from "./write.mjs";
import factCheck from "./fact-check.mjs";
import seo from "./seo.mjs";
import image from "./image.mjs";
import publish from "./publish.mjs";
export const STEPS = {
discovery, research, write,
"fact-check": factCheck, seo, image, publish,
};
Erweiterung später = ein neues File in steps/ + ein neuer Eintrag hier. Ende. Keine Konfig anfassen, keine Routen ändern.
Schritt 5 — Die sieben Step-Module
Jedes Step-Modul hat dieselbe Signatur: default async function(state). Es mutiert den Run-State, ruft saveRun nach jedem wichtigen Update auf (damit das Frontend mitlesen kann), gibt den State zurück.
Discovery (steps/discovery.mjs) — Claude-Call für Keyword-Vorschläge:
import { generateKeywordSuggestions } from "../anthropic.mjs";
import { pillars } from "../pillars.mjs";
import { saveRun, appendLog, setStepStatus } from "../runs.mjs";
export default async function run(state) {
const pillar = pillars[state.pillar];
appendLog(state, "info", `▶ Discovery: Keyword-Lücken zu "${state.title}" suchen …`);
setStepStatus(state, "discovery", "active");
saveRun(state);
const suggestions = await generateKeywordSuggestions({ topic: state.title, pillar });
state.keywordSuggestions = suggestions;
state.cost = "$0.04";
appendLog(state, "ok", `✓ ${suggestions.length} Vorschläge erhalten`);
setStepStatus(state, "discovery", "done", true);
saveRun(state);
return state;
}
Schreiben (steps/write.mjs) — der eigentliche Artikel-Generator:
import { generateArticleBody, generateDescription } from "../anthropic.mjs";
import { pillars } from "../pillars.mjs";
import { saveRun, appendLog, setStepStatus } from "../runs.mjs";
export default async function run(state) {
const pillar = pillars[state.pillar];
const topic = state.selectedKeyword || state.title;
const keywords = state.selectedKeyword
? [state.selectedKeyword, ...pillar.keywords.slice(0, 2)]
: pillar.keywords.slice(0, 3);
setStepStatus(state, "write", "active");
saveRun(state);
const description = await generateDescription({ topic, pillar });
const body = await generateArticleBody({ topic, pillar, keywords });
state.artifacts = { ...state.artifacts, description, body, keywords };
state.cost = "$0.28";
appendLog(state, "ok", `✓ Artikel-Body geschrieben (${body.split(/\s+/).length} Wörter)`);
setStepStatus(state, "write", "done", true);
saveRun(state);
return state;
}
SEO-Check (steps/seo.mjs) — reine Heuristik, kein API-Call. Lokal, kostenlos, sofort:
import { saveRun, appendLog, setStepStatus } from "../runs.mjs";
export default async function run(state) {
setStepStatus(state, "seo", "active");
saveRun(state);
const body = state.artifacts.body || "";
const desc = state.artifacts.description || "";
const checks = [
{ name: "Title-Länge", pass: state.title.length >= 40 && state.title.length <= 65 },
{ name: "Description-Länge", pass: desc.length >= 140 && desc.length <= 165 },
{ name: "H2-Sektionen", pass: (body.match(/^## /gm) || []).length >= 4 },
{ name: "FAQ-Block", pass: /^##\s+FAQ/im.test(body) },
{ name: "Keyword in Intro", pass: body.split(/\s+/).slice(0, 100).join(" ").toLowerCase().includes((state.selectedKeyword || state.title).toLowerCase()) },
];
state.artifacts.seoChecks = checks;
for (const c of checks) {
appendLog(state, c.pass ? "ok" : "warn", `${c.pass ? "✓" : "⚠"} ${c.name}`);
}
setStepStatus(state, "seo", "done", true);
saveRun(state);
return state;
}
Veröffentlichen (steps/publish.mjs) — schreibt MDX, macht git-Operationen:
import fs from "fs";
import path from "path";
import { execSync } from "child_process";
import { pillars } from "../pillars.mjs";
import { buildMdx, writeArticle } from "../frontmatter.mjs";
import { saveRun, appendLog, setStepStatus } from "../runs.mjs";
const ROOT = process.cwd();
export default async function run(state) {
setStepStatus(state, "publish", "active");
saveRun(state);
const pillar = pillars[state.pillar];
const targetDir = path.join(ROOT, pillar.dir);
const mdx = buildMdx({
title: state.title,
description: state.artifacts.description,
pubDate: new Date().toISOString().split("T")[0],
keywords: state.artifacts.keywords || [],
draft: false,
body: state.artifacts.body,
});
writeArticle(targetDir, state.slug, mdx);
appendLog(state, "ok", `✓ Datei angelegt: ${state.slug}.mdx`);
if (process.env.KIFORGE_AUTO_GIT !== "false") {
execSync("git add .", { cwd: ROOT });
execSync(`git commit -m "neuer artikel: ${state.title.replace(/"/g, "")}"`, { cwd: ROOT });
const sha = execSync("git rev-parse --short HEAD", { cwd: ROOT }).toString().trim();
execSync("git push", { cwd: ROOT });
state.publishedCommit = sha;
appendLog(state, "ok", `✓ Pushed: ${sha}`);
}
state.publishedUrl = `/${state.pillar}/${state.slug}/`;
state.status = "done";
setStepStatus(state, "publish", "done", true);
saveRun(state);
return state;
}
Die anderen drei Steps (Research, Fakten-Check, Bild) sind bei mir bewusst Stubs — sie loggen eine Warnung und markieren sich als „done”, damit der Reader durch die Pipeline kommt, aber merkt, wo noch echtes Backing fehlt. So bekomme ich die Pipeline früh end-to-end laufend, statt 3 Wochen mit Bild-Generator-Polishing zu verbringen.
Schritt 6 — HTTP-Server
Zero-Dependency. Native node:http, ~150 Zeilen. scripts/server.mjs:
import { createServer } from "http";
import { listRuns, loadRun, saveRun, deleteRun, createRun, appendLog, setStepStatus } from "./lib/runs.mjs";
import { STEPS } from "./lib/steps/index.mjs";
import { slugify } from "./lib/slugify.mjs";
const PORT = 4322;
function json(res, code, payload) {
res.writeHead(code, {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, POST, DELETE, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type",
});
res.end(JSON.stringify(payload));
}
async function readBody(req) {
return new Promise((resolve) => {
let data = "";
req.on("data", (c) => (data += c));
req.on("end", () => resolve(data ? JSON.parse(data) : {}));
});
}
createServer(async (req, res) => {
if (req.method === "OPTIONS") return res.writeHead(204).end();
const { pathname } = new URL(req.url, `http://localhost:${PORT}`);
if (pathname === "/api/health") return json(res, 200, { ok: true });
if (pathname === "/api/runs" && req.method === "GET") return json(res, 200, listRuns());
if (pathname === "/api/runs" && req.method === "POST") {
const { pillar, title } = await readBody(req);
return json(res, 201, createRun({ pillar, title, slug: slugify(title) }));
}
const m = pathname.match(/^\/api\/runs\/([a-z0-9]+)(?:\/(.*))?$/);
if (m) {
const [, id, rest] = m;
const run = loadRun(id);
if (!run) return json(res, 404, { error: "not found" });
if (!rest && req.method === "GET") return json(res, 200, run);
if (!rest && req.method === "DELETE") { deleteRun(id); return json(res, 200, { ok: true }); }
const step = rest && rest.match(/^steps\/([a-z-]+)\/run$/);
if (step && req.method === "POST") {
const updated = await STEPS[step[1]](run);
// Auto-Advance: nächsten pending Step auf active
const idx = updated.steps.findIndex((s) => s.key === step[1]);
const next = updated.steps[idx + 1];
if (updated.steps[idx].status === "done" && next?.status === "pending") {
next.status = "active";
updated.currentStep = next.key;
saveRun(updated);
}
return json(res, 200, updated);
}
}
json(res, 404, { error: "route not found" });
}).listen(PORT, () => console.log(`API auf :${PORT}`));
Das ist’s. Kein Express, kein Fastify, kein Koa. Native http reicht für ein lokales Dev-Tool — und der Server bootet in 50ms statt 800ms.
CORS ist permissiv (Access-Control-Allow-Origin: *) — das ist ok, weil der Server nur lokal lauscht. Production deployt nichts davon.
Schritt 7 — Frontend-Dashboard
Drei Astro-Pages:
src/pages/dashboard/index.astro— Run-Listesrc/pages/dashboard/runs/[id].astro— Run-Detail mit 7 Step-Cardssrc/pages/dashboard/new.astro— Form für neuen Run
Alle drei sind statische Shells mit data-*-Hooks. Die Daten holt ein Client-Skript via fetch direkt von localhost:4322. Ohne Vite-Proxy, weil Astro 6 sonst die /api/*-Routen abfängt (und 404 antwortet, bevor der Proxy greift).
src/scripts/dashboard-client.ts (Auszug):
const API_BASE = "http://localhost:4322";
const api = {
async listRuns() {
const r = await fetch(`${API_BASE}/api/runs`);
return r.json();
},
async runStep(runId: string, stepKey: string) {
const r = await fetch(`${API_BASE}/api/runs/${runId}/steps/${stepKey}/run`, { method: "POST" });
if (!r.ok) throw new Error((await r.json()).error);
return r.json();
},
async createRun(data: { pillar: string; title: string }) {
const r = await fetch(`${API_BASE}/api/runs`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
});
return r.json();
},
};
document.addEventListener("click", async (ev) => {
const btn = (ev.target as HTMLElement).closest<HTMLButtonElement>("[data-step-action]");
if (!btn) return;
const stepKey = btn.dataset.stepAction!;
const runId = btn.dataset.runId!;
btn.disabled = true;
btn.textContent = "Läuft …";
// Polling alle 1.5s während des Steps für Live-Logs
const poll = setInterval(async () => {
const r = await api.getRun(runId);
renderRunDetail(r);
}, 1500);
try {
const updated = await api.runStep(runId, stepKey);
clearInterval(poll);
renderRunDetail(updated);
} catch (e) {
clearInterval(poll);
btn.disabled = false;
}
});
Wichtig: während ein Step läuft (kann 30–60s dauern bei Discovery oder Schreiben), pollt das Frontend alle 1,5s den aktuellen Run-State und re-rendert. Du siehst die Logs live wachsen, statt 60s ins Nichts zu starren.
Schritt 8 — Pipeline starten und nutzen
Theorie steht, jetzt zum Tippen. Du brauchst zwei Terminals: eins für den API-Server, eins für den Astro-Dev-Server. Beide laufen permanent während du arbeitest.
Einmalig: Dependencies + API-Key
npm install
Dann eine .env im Projekt-Root anlegen, je nach Shell:
Windows CMD:
copy .env.example .env
notepad .env
Windows PowerShell:
Copy-Item .env.example .env
notepad .env
macOS / Linux / Git Bash:
cp .env.example .env
nano .env
In der .env deinen Anthropic-Key eintragen:
ANTHROPIC_API_KEY=sk-ant-deinkey...
Speichern, schließen. Den Key holst du dir unter console.anthropic.com/settings/keys.
Tägliches Hochfahren — beide Terminals
Terminal 1 — API-Server starten:
npm run server
Erfolg sieht so aus:
╭───────────────────────────────────────────╮
│ Dashboard API │
│ http://localhost:4322/api/health │
╰───────────────────────────────────────────╯
Terminal 2 — Astro-Dev-Server starten:
npm run dev
Erfolg:
astro v6.1.9 ready in 1160 ms
Local http://localhost:4321/
Browser auf http://localhost:4321/dashboard/. Du solltest die Run-Liste sehen (anfangs leer).
Quick-Smoke-Test
Bevor du den ersten Run startest, prüf ob beide Server reden:
http://localhost:4322/api/health→ JSON{"ok":true}(Server lebt)http://localhost:4322/api/runs→[](noch keine Runs)http://localhost:4321/dashboard/→ Dashboard-UI lädt ohne den orangen Warning-Banner
Pipeline ohne git-Push testen
Damit der erste Probelauf nichts pusht (z.B. weil dein Repo noch nicht final ist), Server mit ENV-Flag starten — je nach Shell:
Windows CMD:
set KIFORGE_AUTO_GIT=false && npm run server
Windows PowerShell:
$env:KIFORGE_AUTO_GIT="false"; npm run server
macOS / Linux:
KIFORGE_AUTO_GIT=false npm run server
Publish schreibt dann nur die MDX-Datei, überspringt git komplett. Wenn du zufrieden bist, Server stoppen, ohne ENV-Flag neu starten, Publish nochmal — diesmal mit echtem Push.
Stoppen und neu starten
In beiden Terminals: Strg+C, dann J (Windows fragt „Batchvorgang abbrechen? (J/N)”).
Wenn ein Server hängenbleibt und der Port belegt ist:
Windows:
taskkill /F /IM node.exe
macOS / Linux:
pkill node
Killt alle Node-Prozesse — etwas brutal, aber im Dev sicher genug.
Häufige Stolpersteine beim Start
- Banner „API nicht erreichbar” bleibt — Browser hat altes JS gecached. Strg+Shift+R für Hard-Reload.
- Astro startet auf Port 4322 statt 4321 — der Pipeline-Server hat den Port verloren. Beide Prozesse killen, Server zuerst, dann Astro.
ANTHROPIC_API_KEY fehltim Server-Log —.envwird beim Server-Boot gelesen. Nach Edit Server neu starten.- 404 nach Publish auf Live-Site — Astro Dev-Server kennt den frischen MDX-File nicht. Strg+C,
npm run devneu starten.
Mein Beispiel — die Pipeline in Aktion
So sieht ein typischer Run aus, von Idee zu live-deployedem Artikel:
/dashboard/→ „+ Neuer Run”. Säule auswählen, Arbeitstitel eingeben.- Discovery klicken. ~10s warten, Claude liefert 5 Keyword-Vorschläge mit Volumen, Difficulty und „warum für kiforge sinnvoll”.
- Ein Keyword auswählen. Research springt automatisch auf aktiv.
- Research (bei mir Stub — 1s, schreibt Dummy-Briefing).
- Schreiben klicken. ~40s warten, Claude liefert ~1800 Wörter Artikel-Body, plus Meta-Description.
- Fakten-Check (Stub).
- SEO-Check klicken. Sekunde, fünf Heuristiken (Title-Länge, Description-Länge, H2-Count, FAQ-Block, Keyword im Intro).
- Bild generieren (Stub).
- Veröffentlichen klicken. MDX wird in
src/content/<säule>/<slug>.mdxgeschrieben,git add . && git commit && git push. Cloudflare Pages baut, in 2–3 Minuten ist der Artikel live.
Häufige Fehler und Lösungen
Was ich beim Bauen selbst getroffen habe — chronologisch, damit du es nicht nochmal machst:
„Vite-Proxy für /api funktioniert nicht.”
Astro 6 fängt /api/* ab, bevor der Vite-Proxy greift, und antwortet 404. Lösung: kein Proxy, sondern direkt http://localhost:4322 im Client. CORS auf dem Server-json()-Helper mit * reicht für lokales Dev.
„Ein Step bleibt auf pending stehen, obwohl der vorherige done ist.”
Auto-Advance fehlt. Im Server nach await stepFn(run) den nächsten pending Step explizit auf active setzen — dann ist sein Button im Frontend klickbar.
„Astro Dev-Server kennt einen frisch erstellten Artikel nicht.”
Content Collection cached beim Start. Nach git push und dem MDX-Schreiben einmal Strg+C, npm run dev — beim Neustart wird die Collection frisch eingelesen.
„trailingSlash: 'always' killt alle Links.”
Im Dev-Mode antwortet Astro mit 404, wenn ein Link ohne Slash kommt. trailingSlash: 'ignore' (Astro-Default) reicht — build.format: 'directory' sorgt sowieso für saubere Production-URLs.
„Server crasht silent, Logs zeigen [404] mit Brackets.”
Brackets-Format = Astro, mein Server logt ohne Brackets. Wenn der Server nicht läuft, antwortet Astro auf 4322 (Port-Konflikt). taskkill /F /IM node.exe, dann Server vor Astro starten.
„Keyword-Vorschläge JSON parst nicht.”
Claude wickelt Output manchmal in ```json -Blöcke. Strippen: .replace(/^```(?:json)?\n?/, "").replace(/\n?```$/, "").
Was kommt als Nächstes?
Drei Erweiterungen, die ich gerade vorbereite (oder vorbereitet habe — siehe GitHub-Repo):
- Research-Step echt — Claude mit dem Web-Search-Tool, schreibt ein
briefing.md, dasSchreibenals Kontext bekommt - Bild-Step echt — entweder Replicate-API (teuer, hochwertig) oder ein deterministischer SVG-Generator mit kiforge-Brandkit (kostenlos, schnell)
- SEO-Score-Step — über die aktuelle Heuristik hinaus: keyword-density-Auswertung, interne-Link-Suggestions, ähnliche Artikel im eigenen Repo finden
Wenn du Lust auf einen der drei hast: das Pattern ist immer gleich — neue Datei in scripts/lib/steps/, default async function(state), appendLog, saveRun, fertig.
Newsletter: Wöchentlich mit aktuellen News, Tutorials und Workflows zu MCP, Agents und lokalen LLMs. Hier abonnieren.
Live-Demo dazu: das Dashboard in Aktion
Wenn du lieber zuschaust statt mitliest: hier siehst du die komplette Pipeline einmal von vorn bis hinten — Run anlegen, Discovery, Keyword wählen, Schreiben, SEO-Check, Veröffentlichen, fertiger Artikel auf der Live-Site.
Live-Demo: kiforge-Pipeline von Idee zu Live
YouTube-Embed wird hier eingesetzt, sobald hochgeladen.
Was du im Video konkret siehst: Run anlegen mit dem Titel „SEO-Recherche-MCP für Notion”, durch alle sieben Steps klicken, Live-Logs mitlaufen sehen, am Ende den frisch deployedten Artikel auf kiforge.de öffnen. Ohne Editor-Wechsel, ohne Copy-Paste, ohne Terminal.
FAQ — Häufige Fragen zu MCP-Servern und Content-Pipelines
Was ist der Unterschied zwischen einem MCP-Server und meinem HTTP-Server hier?
Ein echter MCP-Server folgt dem Anthropic-Protokoll und kann von LLM-Clients wie Claude Desktop direkt eingebunden werden. Mein Setup hier ist ein eigener HTTP-Server, der dieselbe Logik orchestriert, aber für ein eigenes Frontend gedacht ist. Wer den nächsten Schritt machen will, tauscht den HTTP-Layer gegen das offizielle MCP-SDK — die Step-Module bleiben identisch.
Wie viel kostet ein Pipeline-Durchlauf?
Bei mir aktuell ca. $0.30 pro Artikel — Discovery $0.04, Schreiben $0.28, Rest gratis (Stubs oder lokale Heuristik). Mit echtem Research-Step kommen vielleicht $0.10–0.20 dazu. Bild-Generierung via Replicate je nach Modell $0.01–0.05 pro Bild.
Brauche ich Claude Desktop für diese Pipeline?
Nein, in dieser Version nicht. Wir nutzen die Claude API direkt über das @anthropic-ai/sdk. Claude Desktop wäre nur nötig, wenn du den Workflow als echten MCP-Server umbauen willst, sodass Claude Desktop ihn direkt aufruft.
Kann ich das auch ohne Cloudflare Pages deployen?
Ja. Astro produziert plain HTML/CSS/JS, du kannst auf Netlify, Vercel, GitHub Pages, S3 deployen. Der Publish-Step macht git push — der Rest ist Sache des Hosts.
Wie verhindere ich, dass git auto-pusht beim Testen?
Server mit KIFORGE_AUTO_GIT=false starten. Dann schreibt Publish nur die MDX-Datei, überspringt git komplett. Praktisch beim ersten Durchlauf oder wenn dein Token noch fehlt.
Wie schließe ich neue Pipeline-Schritte an?
Drei Touchpoints: Erstens, die Step-Liste in der runs-Library erweitern. Zweitens, eine neue Datei unter scripts/lib/steps/ anlegen mit Default-Export. Drittens, in der STEPS-Map einen Eintrag dafür ergänzen. Frontend rendert automatisch.
Wieso nicht einfach n8n / Make / Zapier?
Kannst du. Wenn dir das reicht und du nicht in TypeScript schreibst — los. Mein Grund für Eigenbau: voller Kontrolle über die Prompts, Voice-Block in der API-Wrapper, Code-Review für jeden Step ohne Vendor-Lock-in. Plus Zero-Cost-Hosting (alles lokal, nur API-Calls kosten).
Was, wenn ich keine 7 Steps brauche?
Die Pipeline-Definition steckt in einer einzigen Liste — kürze sie. Drei Steps reichen für eine simple Topic→Body→Publish-Pipeline. Dashboard rendert automatisch nur die definierten Schritte.
Fazit
Du hast jetzt alles, was du brauchst, um eine eigene MCP-konforme Content-Pipeline zu bauen. Astro-Frontend, Node-HTTP-Server, sieben Step-Module, Auto-Deploy. Alles erweiterbar nach demselben Pattern — neue Datei in den Steps, eine Zeile in der Registry, fertig.
Drei pragmatische nächste Schritte:
- Du willst’s in Aktion sehen? Hier ist das Video — kompletter Run von Idee zu Live: oben in der Live-Demo-Sektion.
- Du willst’s nachbauen? Hier ist der Artikel oben, plus das GitHub-Repo mit dem vollständigen Code zum Forken.
- Du willst regelmäßig solche Setups bekommen? Newsletter abonnieren — wöchentlich, mit News & Tutorials, kein Spam.
Mit Claude Cowork bauen lassen
Du willst nicht jeden Schritt manuell durchgehen? Dann der Shortcut: ich habe eine Markdown-Datei mit kompletten Anweisungen für Claude Cowork-Modus vorbereitet. Drag-and-drop sie in eine neue Cowork-Session, und Claude scaffoldet das ganze Setup für dich.
So gehst du vor:
- Klick auf den Download-Button unten und speicher die
kiforge-cowork-instructions.mdlokal. - Öffne den Claude-Desktop-Client und starte eine neue Cowork-Session in einem leeren Ordner deiner Wahl.
- Zieh die
.md-Datei in den Chat oder häng sie als Attachment an. - Schreib einen Satz: „Bau mir das Projekt aus dieser Anleitung.”
- Lehn dich zurück. Claude installiert Astro, legt alle Dateien an, schreibt den Server-Code, das Step-System, das Dashboard-Frontend.
Was du danach noch selbst machst: .env mit deinem Anthropic-Key füllen, einmal npm run server plus npm run dev starten, und du bist drin.
Cowork-Recipe
kiforge-cowork-pipeline-seo.md
Komplette Build-Anleitung für Claude Cowork — Architektur, alle Code-Files, Boot-Sequenz.
📬 Du bekommst die .md-Datei sofort per E-Mail. Plus den wöchentlichen Newsletter — jederzeit abmeldbar.
Hinweis zum Cowork-Modus: Cowork ist Claudes Desktop-Mode mit direktem Datei-Zugriff auf einen ausgewählten Ordner — er kann lesen, schreiben, Befehle ausführen. Wer Cowork noch nicht kennt: Doku bei Anthropic .
Wenn du beim Nachbau hängenbleibst, schreib mir kurz — Mail steht im Footer. „Bei mir hat das nicht funktioniert”-Berichte machen den Artikel besser.