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:
@@ -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');
|
||||
Reference in New Issue
Block a user