feat: single-image transform, NAS/FTP in delivery, backup-all any target, metadata sidecar, prompt view, preset manager, API token + MCP server

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNQ8ghPfzAfsyVYd6HgFb6
This commit is contained in:
2026-07-23 21:29:43 +00:00
parent 5cfafb456c
commit 295f0ce91e
19 changed files with 479 additions and 69 deletions
+41
View File
@@ -0,0 +1,41 @@
# Klarbild MCP-Server
Erlaubt einem KI-Assistenten (z. B. Claude), Bilder an Klarbild zu übergeben und dort zu
verarbeiten — etwa „nimm die Bilder aus Ordner XY" oder „verarbeite die Fotos, die mir jemand
per iMessage geschickt hat". Der Assistent besorgt die Bilddateien (mit seinen eigenen
Werkzeugen) und ruft dann die Klarbild-Tools auf.
## Einrichtung
1. In Klarbild einen Token erzeugen: **Admin → „Automatisierung / MCP-Zugriff" → Token erzeugen**.
2. Abhängigkeit installieren: `npm i @modelcontextprotocol/sdk`
3. Server starten (bzw. im MCP-Client eintragen):
```json
{
"mcpServers": {
"klarbild": {
"command": "node",
"args": ["/pfad/zu/mcp/klarbild-mcp.mjs"],
"env": {
"KLARBILD_URL": "https://klarbild.heidrich-digital.de",
"KLARBILD_TOKEN": "klb_…"
}
}
}
}
```
## Tools
- **list_recipes** — verfügbare Presets/Rezepte auflisten.
- **process_images** — Bilder (lokale Pfade oder `data:`-URLs) hochladen und verarbeiten.
Entweder `recipeId` (Preset) oder `mode` (`each`/`compose`/`generate`) + Optionen
(`prompt_text`, `output_format`, `delivery`). Liefert die Auftrags-ID.
- **job_status** — Status eines Auftrags abfragen.
## Sicherheit
Der Token gibt vollen programmatischen Zugriff auf Upload/Verarbeitung (nicht auf den
Admin-Bereich). Wie ein Passwort behandeln; bei Verdacht neuen Token erzeugen (der alte wird
dadurch ungültig).
+98
View File
@@ -0,0 +1,98 @@
#!/usr/bin/env node
// Klarbild MCP-Server — erlaubt einem KI-Assistenten (z. B. Claude), lokale Bilder
// oder anderweitig beschaffte Bilder (iMessage, Ordner XY …) an Klarbild zu übergeben
// und dort zu verarbeiten. Der Assistent besorgt die Bilddateien; dieser Server lädt
// sie hoch und legt einen Auftrag an.
//
// Setup:
// npm i @modelcontextprotocol/sdk
// KLARBILD_URL=https://klarbild.heidrich-digital.de \
// KLARBILD_TOKEN=klb_xxx node mcp/klarbild-mcp.mjs
// Den Token erzeugt man in Klarbild unter Admin → „Automatisierung / MCP-Zugriff".
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { readFile } from 'node:fs/promises';
import { basename } from 'node:path';
const BASE = (process.env.KLARBILD_URL || '').replace(/\/$/, '');
const TOKEN = process.env.KLARBILD_TOKEN || '';
if (!BASE || !TOKEN) { console.error('KLARBILD_URL und KLARBILD_TOKEN müssen gesetzt sein.'); process.exit(1); }
const authHeaders = { Authorization: `Bearer ${TOKEN}` };
const api = (path) => `${BASE}${path}`;
async function uploadFile(pathOrBase64, name) {
let buf, filename = name;
if (pathOrBase64.startsWith('data:')) {
buf = Buffer.from(pathOrBase64.split(',')[1], 'base64');
filename = name || 'bild.png';
} else {
buf = await readFile(pathOrBase64);
filename = name || basename(pathOrBase64);
}
const fd = new FormData();
fd.append('files', new Blob([buf]), filename);
const r = await fetch(api('/api/uploads'), { method: 'POST', headers: authHeaders, body: fd });
const j = await r.json();
const info = (j.files || [])[0];
if (!info?.source_path) throw new Error(info?.error || 'Upload fehlgeschlagen');
return { source_path: info.source_path, filename };
}
const server = new Server({ name: 'klarbild', version: '1.0.0' }, { capabilities: { tools: {} } });
const TOOLS = [
{ name: 'list_recipes', description: 'Verfügbare Presets/Rezepte in Klarbild auflisten.',
inputSchema: { type: 'object', properties: {} } },
{ name: 'process_images',
description: 'Bilder an Klarbild übergeben und verarbeiten. Dateien als lokale Pfade ODER data:-URLs. Entweder recipeId ODER mode+Optionen angeben.',
inputSchema: { type: 'object', properties: {
files: { type: 'array', items: { type: 'string' }, description: 'Lokale Pfade oder data:-URLs' },
recipeId: { type: 'string', description: 'Optional: Preset-ID (aus list_recipes)' },
mode: { type: 'string', enum: ['each', 'compose', 'generate'], description: 'Falls kein recipeId' },
prompt_text: { type: 'string', description: 'Beschreibung für compose/generate' },
output_format: { type: 'string', description: 'z. B. 30x40, theframe, keep' },
delivery: { type: 'string', enum: ['library', 'picdrop', 'both'] },
}, required: ['files'] } },
{ name: 'job_status', description: 'Status eines Auftrags abfragen.',
inputSchema: { type: 'object', properties: { jobId: { type: 'string' } }, required: ['jobId'] } },
];
server.setRequestHandler({ method: 'tools/list' }, async () => ({ tools: TOOLS }));
server.setRequestHandler({ method: 'tools/call' }, async (req) => {
const { name, arguments: a = {} } = req.params;
try {
if (name === 'list_recipes') {
const j = await (await fetch(api('/api/recipes'), { headers: authHeaders })).json();
const list = (j.recipes || []).map((r) => `${r.id}${r.name} (${r.mode || 'each'})`).join('\n');
return { content: [{ type: 'text', text: list || 'Keine Presets.' }] };
}
if (name === 'process_images') {
const files = a.files || [];
const mode = a.mode || 'each';
const sources = (mode === 'generate') ? [] : await Promise.all(files.map((f) => uploadFile(f)));
const body = a.recipeId
? { recipeId: a.recipeId, mode, sources, prompt_text: a.prompt_text }
: { mode, prompt_text: a.prompt_text, sources,
recipe: { tasks: a.output_format && a.output_format !== 'keep' ? ['format'] : [],
output_format: a.output_format || 'keep', delivery: a.delivery || 'library' },
delivery: a.delivery || 'library' };
const j = await (await fetch(api('/api/jobs'), { method: 'POST', headers: { ...authHeaders, 'Content-Type': 'application/json' }, body: JSON.stringify(body) })).json();
if (!j.jobId) throw new Error(j.error || 'Auftrag fehlgeschlagen');
return { content: [{ type: 'text', text: `Auftrag angelegt: ${j.jobId}` }] };
}
if (name === 'job_status') {
const j = await (await fetch(api(`/api/jobs/${a.jobId}`), { headers: authHeaders })).json();
const job = j.job || j;
return { content: [{ type: 'text', text: `Status: ${job.status}${job.done_count}/${job.total} fertig${job.failed_count ? `, ${job.failed_count} fehlgeschlagen` : ''}` }] };
}
return { content: [{ type: 'text', text: `Unbekanntes Tool: ${name}` }], isError: true };
} catch (e) {
return { content: [{ type: 'text', text: `Fehler: ${e?.message || e}` }], isError: true };
}
});
await server.connect(new StdioServerTransport());
console.error('[klarbild-mcp] bereit');