@prontosite/mcp 0.13.2 → 0.13.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +23 -0
- package/api.mjs +96 -12
- package/index.mjs +2 -2
- package/package.json +1 -1
- package/tools.mjs +59 -10
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,29 @@
|
|
|
3
3
|
Alle nennenswerten Änderungen am MCP-Server für ProntoSite. Der Server ist ein dünner Client um die
|
|
4
4
|
ProntoSite Deploy-API; die eigentliche Logik lebt serverseitig.
|
|
5
5
|
|
|
6
|
+
## 0.13.4 – 2026-10-19
|
|
7
|
+
|
|
8
|
+
Enthält 0.13.3 (nie einzeln veröffentlicht).
|
|
9
|
+
- Neu: `site_undo {site, steps?, baseRev?}` / `site_redo` – derselbe Rückgängig-Verlauf wie im Dashboard; Antwort je Schritt mit
|
|
10
|
+
Op-Typ, Seite(n), Akteur (`vonAnderem:true`, wenn die Änderung von jemand anderem stammte). Instructions: „Bei ‚rückgängig‘ site_undo,
|
|
11
|
+
nie Dateien manuell zurückbauen.“
|
|
12
|
+
- Neu: `site_media_delete {site, mediaId, force?}` → Papierkorb (30 Tage, nie endgültig); noch verwendet (Entwurf oder
|
|
13
|
+
veröffentlicht) → `inUse` mit Fundstellen, keine Löschung ohne `force` (nur Uploads; Import-Bilder nur unbenutzt).
|
|
14
|
+
`site_media_restore {trashId}` holt zurück; `site_media_list` zeigt den Papierkorb (`trash`).
|
|
15
|
+
- `site_image_set` erhält das responsive Bild: srcset aus den Server-Varianten, vorhandenes `sizes`, `<picture>`-Quellen umgeschrieben.
|
|
16
|
+
- `site_preview_screenshot`: Einblend-/Scroll-Animationen (ohne JS sonst unsichtbar) werden sichtbar dargestellt.
|
|
17
|
+
|
|
18
|
+
## 0.13.3 – 2026-10-19
|
|
19
|
+
|
|
20
|
+
- Neu (nur lokales Paket): `media_upload_file {site, path, purpose?}` – lokale Datei in einem Schritt hochladen
|
|
21
|
+
(Upload-Link + PUT + Übernahme) → `mediaId`. Hilfreich, wenn die Sandbox des Assistenten (z. B. Claude Desktop/Cowork)
|
|
22
|
+
app.prontosite.io nicht erreicht, der lokale Server aber schon.
|
|
23
|
+
- Prüfung VOR jedem Netzaufruf: nur unterhalb des Home-Ordners, kein `..`, kein Symlink in der Kette (Datei oder Ordner),
|
|
24
|
+
keine versteckten Ordner (`~/.ssh` …), nur reguläre Datei, Größe vor dem Lesen (max. 15 MB), Magic-Bytes + passende Endung
|
|
25
|
+
(JPG/PNG/WebP/SVG/PDF). Abgelehnte Dateien verlassen den Rechner nicht.
|
|
26
|
+
- `media_upload_begin` nimmt kein `path` mehr an (bisher ungeprüfter Lese-Weg) – lokale Dateien nur noch über `media_upload_file`.
|
|
27
|
+
- Server-Instructions lokal: „Wenn du einen lokalen Dateipfad hast, nutze media_upload_file; sonst Upload-Link.“
|
|
28
|
+
|
|
6
29
|
## 0.13.2 – 2026-10-19
|
|
7
30
|
|
|
8
31
|
- Upload-Beschreibung: Bilder und PDF; PDFs mit Skripten, automatischen Aktionen, Formular-Versand oder Anhängen werden abgelehnt;
|
package/api.mjs
CHANGED
|
@@ -415,20 +415,85 @@ export async function apiPageCopy(cfg, { site, from, to, baseRev = null } = {},
|
|
|
415
415
|
method: "POST", headers: { ...authHeaders(token, cfg), "content-type": "application/json" }, body: JSON.stringify({ from, to, ...(Number.isInteger(baseRev) ? { baseRev } : {}) }) }));
|
|
416
416
|
}
|
|
417
417
|
|
|
418
|
-
/** POST /api/media-upload/<site> – Upload-Link (15 Min., einmalig) für ein neues Bild
|
|
419
|
-
export async function apiMediaUploadBegin(cfg, { site, purpose = null, filename = null
|
|
418
|
+
/** POST /api/media-upload/<site> – Upload-Link (15 Min., einmalig) für ein neues Bild/Dokument. Lokale Datei: apiMediaUploadFile. */
|
|
419
|
+
export async function apiMediaUploadBegin(cfg, { site, purpose = null, filename = null } = {}, fetchImpl = fetch) {
|
|
420
420
|
const { token, slug } = resolveTarget(cfg, site);
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
421
|
+
return asJson(await fetchImpl(`${baseUrl(cfg)}/media-upload/${encodeURIComponent(slug)}`, {
|
|
422
|
+
method: "POST", headers: { ...authHeaders(token, cfg), "content-type": "application/json" }, body: JSON.stringify({ purpose, filename }) }));
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// Lokaler Datei-Upload (nur stdio-Paket): dieselben Grenzen wie der Server (MEDIA_MAX_FILE_MB, Typen aus sniffImageMime +
|
|
426
|
+
// doc-check), aber VOR jedem Netzaufruf – eine abgelehnte Datei verlässt den Rechner nie.
|
|
427
|
+
export const LOCAL_UPLOAD_MAX_BYTES = 15 * 1024 * 1024;
|
|
428
|
+
const LOCAL_UPLOAD_TYPES = { png: ["png"], jpeg: ["jpg", "jpeg"], webp: ["webp"], svg: ["svg"], pdf: ["pdf"] };
|
|
429
|
+
function sniffLocalType(b) {
|
|
430
|
+
if (b.length >= 8 && b[0] === 0x89 && b[1] === 0x50 && b[2] === 0x4e && b[3] === 0x47) return "png";
|
|
431
|
+
if (b.length >= 3 && b[0] === 0xff && b[1] === 0xd8 && b[2] === 0xff) return "jpeg";
|
|
432
|
+
if (b.length >= 12 && b.toString("ascii", 0, 4) === "RIFF" && b.toString("ascii", 8, 12) === "WEBP") return "webp";
|
|
433
|
+
if (b.length >= 5 && b.toString("ascii", 0, 5) === "%PDF-") return "pdf";
|
|
434
|
+
const head = b.subarray(0, 256).toString("utf8").replace(/^/, "").trimStart().toLowerCase();
|
|
435
|
+
if (head.startsWith("<svg") || (head.startsWith("<?xml") && /<svg[\s>]/.test(b.subarray(0, 4096).toString("utf8").toLowerCase()))) return "svg";
|
|
436
|
+
return null;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* Lokale Datei sicher lesen: nur unterhalb des Nutzer-Home, kein Symlink in der Kette (auch nicht als Verzeichnis),
|
|
441
|
+
* keine versteckten Ordner/Dateien (~/.ssh, ~/.aws …), nur reguläre Datei, Größe VOR dem Lesen, Magic-Bytes + passende
|
|
442
|
+
* Endung (JPG/PNG/WebP/SVG/PDF). Wirft mit deutscher Meldung; liest erst, wenn alle Pfad-Prüfungen bestanden sind.
|
|
443
|
+
* @returns {{ buf: Buffer, name: string, type: string, bytes: number, path: string }}
|
|
444
|
+
*/
|
|
445
|
+
export function readLocalUploadFile(p, { home = os.homedir(), maxBytes = LOCAL_UPLOAD_MAX_BYTES } = {}) {
|
|
446
|
+
if (typeof p !== "string" || !p.trim() || p.includes("\0")) throw new Error("path fehlt – absoluten Dateipfad angeben (z. B. ~/Bilder/team.jpg).");
|
|
447
|
+
let raw = p.trim();
|
|
448
|
+
if (raw.split(/[\\/]+/).includes("..")) throw new Error("Pfad mit „..“ ist nicht erlaubt.");
|
|
449
|
+
if (raw === "~" || raw.startsWith("~/")) raw = path.join(home, raw.slice(1));
|
|
450
|
+
if (!path.isAbsolute(raw)) throw new Error("Bitte einen absoluten Dateipfad angeben (z. B. ~/Bilder/team.jpg).");
|
|
451
|
+
const abs = path.resolve(raw);
|
|
452
|
+
const homeLex = path.resolve(home);
|
|
453
|
+
let homeReal; try { homeReal = fs.realpathSync(homeLex); } catch { throw new Error("Nutzer-Home nicht lesbar."); }
|
|
454
|
+
const prefix = [homeLex, homeReal].find(h => abs.startsWith(h + path.sep));
|
|
455
|
+
if (!prefix) throw new Error("Nur Dateien unterhalb deines Benutzerordners (Home) sind erlaubt.");
|
|
456
|
+
const parts = abs.slice(prefix.length + 1).split(path.sep).filter(Boolean);
|
|
457
|
+
if (parts.some(x => x.startsWith("."))) throw new Error("Versteckte Dateien/Ordner (z. B. ~/.ssh) sind nicht erlaubt.");
|
|
458
|
+
let cur = homeReal;
|
|
459
|
+
for (const part of parts) { // jedes Glied einzeln: kein Symlink, auch nicht als Zwischenordner
|
|
460
|
+
cur = path.join(cur, part);
|
|
461
|
+
let st; try { st = fs.lstatSync(cur); } catch { throw new Error(`Datei nicht gefunden: ${p}`); }
|
|
462
|
+
if (st.isSymbolicLink()) throw new Error("Symlinks sind nicht erlaubt – bitte den echten Dateipfad angeben.");
|
|
430
463
|
}
|
|
431
|
-
|
|
464
|
+
if (fs.realpathSync(cur) !== cur) throw new Error("Pfad nicht eindeutig (Link in der Kette) – bitte den echten Dateipfad angeben.");
|
|
465
|
+
let fd;
|
|
466
|
+
try { fd = fs.openSync(cur, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW || 0)); }
|
|
467
|
+
catch { throw new Error(`Datei nicht lesbar: ${p}`); }
|
|
468
|
+
try {
|
|
469
|
+
const st = fs.fstatSync(fd); // am offenen Handle prüfen: kein Austausch zwischen Prüfen und Lesen
|
|
470
|
+
if (!st.isFile()) throw new Error("Nur reguläre Dateien sind erlaubt (kein Ordner, kein Gerät).");
|
|
471
|
+
if (!st.size) throw new Error("Leere Datei.");
|
|
472
|
+
if (st.size > maxBytes) throw new Error(`Datei zu groß (${(st.size / 1048576).toFixed(1)} MB, max. ${Math.round(maxBytes / 1048576)} MB).`);
|
|
473
|
+
const buf = Buffer.alloc(st.size);
|
|
474
|
+
let off = 0;
|
|
475
|
+
while (off < st.size) { const n = fs.readSync(fd, buf, off, st.size - off, off); if (!n) break; off += n; }
|
|
476
|
+
if (off !== st.size) throw new Error("Datei hat sich beim Lesen verändert – bitte erneut versuchen.");
|
|
477
|
+
const type = sniffLocalType(buf);
|
|
478
|
+
if (!type) throw new Error("Kein unterstützter Dateityp – erlaubt sind JPG, PNG, WebP, SVG und PDF.");
|
|
479
|
+
const name = path.basename(cur);
|
|
480
|
+
const ext = (path.extname(name).slice(1) || "").toLowerCase();
|
|
481
|
+
if (!LOCAL_UPLOAD_TYPES[type].includes(ext)) throw new Error(`Dateiendung „.${ext}“ passt nicht zum Inhalt (${type.toUpperCase()}).`);
|
|
482
|
+
return { buf, name, type, bytes: st.size, path: cur };
|
|
483
|
+
} finally { fs.closeSync(fd); }
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/** Lokale Datei in EINEM Schritt hochladen: prüfen (ohne Netz) → media-upload → PUT → media-commit → mediaId. */
|
|
487
|
+
export async function apiMediaUploadFile(cfg, { site, path: localPath, purpose = null } = {}, fetchImpl = fetch, opts = {}) {
|
|
488
|
+
const f = readLocalUploadFile(localPath, opts);
|
|
489
|
+
const out = await apiMediaUploadBegin(cfg, { site, purpose, filename: f.name }, fetchImpl);
|
|
490
|
+
if (Number.isFinite(out.maxBytes) && f.bytes > out.maxBytes) throw new Error(`Datei zu groß (max. ${Math.round(out.maxBytes / 1048576)} MB).`);
|
|
491
|
+
const up = await fetchImpl(out.uploadUrl, { method: "PUT", headers: { "x-filename": encodeURIComponent(f.name) }, body: f.buf });
|
|
492
|
+
const r = await up.json().catch(() => ({}));
|
|
493
|
+
if (!up.ok) throw new Error(r.error || `Upload fehlgeschlagen (HTTP ${up.status}).`);
|
|
494
|
+
const m = await apiMediaCommit(cfg, { site, uploadId: out.uploadId }, fetchImpl);
|
|
495
|
+
if (m.pending) throw new Error(m.hinweis || "Upload nicht angekommen – bitte erneut versuchen.");
|
|
496
|
+
return { ...m, datei: f.name, bytes: f.bytes };
|
|
432
497
|
}
|
|
433
498
|
/** POST /api/media-commit/<site> – hochgeladene Datei prüfen und als Medium ablegen → mediaId. */
|
|
434
499
|
export async function apiMediaCommit(cfg, { site, uploadId } = {}, fetchImpl = fetch) {
|
|
@@ -450,6 +515,25 @@ export async function apiImageSet(cfg, { site, page, nodeId = null, marker = nul
|
|
|
450
515
|
method: "POST", headers: { ...authHeaders(token, cfg), "content-type": "application/json" },
|
|
451
516
|
body: JSON.stringify({ page, mediaId, ...(nodeId ? { nodeId } : {}), ...(marker ? { marker } : {}), ...(alt != null ? { alt } : {}), ...(Number.isInteger(baseRev) ? { baseRev } : {}) }) }));
|
|
452
517
|
}
|
|
518
|
+
/** POST /api/undo|redo/<site> – letzte Änderung(en) des Entwurfs zurücknehmen bzw. wiederherstellen (derselbe Stack wie das Dashboard). */
|
|
519
|
+
export async function apiUndo(cfg, { site, steps = 1, baseRev = null, redo = false } = {}, fetchImpl = fetch) {
|
|
520
|
+
const { token, slug } = resolveTarget(cfg, site);
|
|
521
|
+
return asJsonOr409(await fetchImpl(`${baseUrl(cfg)}/${redo ? "redo" : "undo"}/${encodeURIComponent(slug)}`, {
|
|
522
|
+
method: "POST", headers: { ...authHeaders(token, cfg), "content-type": "application/json" },
|
|
523
|
+
body: JSON.stringify({ ...(Number.isInteger(steps) ? { steps } : {}), ...(Number.isInteger(baseRev) ? { baseRev } : {}) }) }));
|
|
524
|
+
}
|
|
525
|
+
/** POST /api/media-delete/<site> – Medium in den Papierkorb (30 Tage); noch verwendet → 409-Objekt mit usage (force nur für Uploads). */
|
|
526
|
+
export async function apiMediaDelete(cfg, { site, mediaId, force = false } = {}, fetchImpl = fetch) {
|
|
527
|
+
const { token, slug } = resolveTarget(cfg, site);
|
|
528
|
+
return asJsonOr409(await fetchImpl(`${baseUrl(cfg)}/media-delete/${encodeURIComponent(slug)}`, {
|
|
529
|
+
method: "POST", headers: { ...authHeaders(token, cfg), "content-type": "application/json" }, body: JSON.stringify({ mediaId, ...(force === true ? { force: true } : {}) }) }));
|
|
530
|
+
}
|
|
531
|
+
/** POST /api/media-restore/<site> – Medium aus dem Papierkorb zurückholen. */
|
|
532
|
+
export async function apiMediaRestore(cfg, { site, trashId } = {}, fetchImpl = fetch) {
|
|
533
|
+
const { token, slug } = resolveTarget(cfg, site);
|
|
534
|
+
return asJson(await fetchImpl(`${baseUrl(cfg)}/media-restore/${encodeURIComponent(slug)}`, {
|
|
535
|
+
method: "POST", headers: { ...authHeaders(token, cfg), "content-type": "application/json" }, body: JSON.stringify({ trashId }) }));
|
|
536
|
+
}
|
|
453
537
|
/** GET /api/preview-screenshot/<site>?page=&width= – PNG des Entwurfs (Base64 + Maße). */
|
|
454
538
|
export async function apiPreviewScreenshot(cfg, { site, page = "index.html", width = 1280 } = {}, fetchImpl = fetch) {
|
|
455
539
|
const { token, slug } = resolveTarget(cfg, site);
|
package/index.mjs
CHANGED
|
@@ -14,14 +14,14 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
14
14
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
15
15
|
import { z } from "zod";
|
|
16
16
|
import { loadConfig } from "./api.mjs";
|
|
17
|
-
import { registerTools,
|
|
17
|
+
import { registerTools, instructionsFor } from "./tools.mjs";
|
|
18
18
|
|
|
19
19
|
const cfg = loadConfig();
|
|
20
20
|
// Deutliche Startwarnung bei ambiger Konfiguration (Top-Level-Token NEBEN sites-Map). Nach stderr, damit
|
|
21
21
|
// es im MCP-Log/Terminal auffällt; die endgültige Auflösung (Betreuer-Token-Vorrang vs. Map) klärt sites_list.
|
|
22
22
|
for (const w of (cfg.warnings || [])) process.stderr.write("[prontosite-mcp] ⚠ " + w + "\n");
|
|
23
23
|
// instructions: Arbeitsweise (Weg B = Standard) – gleicher Text wie am Remote-Endpunkt.
|
|
24
|
-
const server = new McpServer({ name: "prontosite", version: "0.13.
|
|
24
|
+
const server = new McpServer({ name: "prontosite", version: "0.13.4" }, { instructions: instructionsFor("local") });
|
|
25
25
|
// Tool-Definitionen: gemeinsame Quelle mit dem Remote-Endpunkt (tools.mjs, Konzept KI-Anbindung §4.2).
|
|
26
26
|
registerTools(server, { z, cfg, mode: "local" });
|
|
27
27
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@prontosite/mcp",
|
|
3
|
-
"version": "0.13.
|
|
3
|
+
"version": "0.13.4",
|
|
4
4
|
"description": "MCP-Server für ProntoSite – Website-Status, Export, Deploy und Veröffentlichen direkt aus einem KI-Assistenten (Claude Desktop). Dünner Client um die ProntoSite Deploy-API.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/tools.mjs
CHANGED
|
@@ -9,13 +9,13 @@
|
|
|
9
9
|
* site_domain_connect nur mit Betreuer-Token (Entscheidung 9a).
|
|
10
10
|
* `z` wird vom Aufrufer übergeben (dieselbe zod-Instanz wie sein MCP-SDK).
|
|
11
11
|
*/
|
|
12
|
-
import { apiEditText, apiPageTexts, apiPageTextsSet, apiPageCopy, apiMediaUploadBegin, apiMediaCommit, apiMediaList, apiImageSet, apiPreviewScreenshot, apiStatus, apiExport, apiExportFetch, apiDeploy, apiPublish, sitesList, apiAccountCreate, apiCompanyGet, apiCompanySet, apiLegalOptions, apiDomainStatus, apiDomainConnect, apiEmbeds, apiEmbedConsent, apiWidgets, apiWidgetSet, apiFiles, apiFileGet, apiFilePut, apiFileDelete, apiFileRename, apiLock } from "./api.mjs";
|
|
12
|
+
import { apiEditText, apiPageTexts, apiPageTextsSet, apiPageCopy, apiMediaUploadBegin, apiMediaUploadFile, apiMediaCommit, apiMediaList, apiImageSet, apiUndo, apiMediaDelete, apiMediaRestore, apiPreviewScreenshot, apiStatus, apiExport, apiExportFetch, apiDeploy, apiPublish, sitesList, apiAccountCreate, apiCompanyGet, apiCompanySet, apiLegalOptions, apiDomainStatus, apiDomainConnect, apiEmbeds, apiEmbedConsent, apiWidgets, apiWidgetSet, apiFiles, apiFileGet, apiFilePut, apiFileDelete, apiFileRename, apiLock } from "./api.mjs";
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* Arbeitsweise (MCP-`instructions` beim Initialize – lokal UND /mcp): Weg B ist der Standard (Nick 19.10.).
|
|
16
16
|
* Direkt am Server-Entwurf arbeiten; Export + Deploy nur auf ausdrücklichen Wunsch bzw. für Komplett-Umbauten.
|
|
17
17
|
*/
|
|
18
|
-
|
|
18
|
+
const instructionLines = mode => [
|
|
19
19
|
"ProntoSite – so arbeitest du mit einer Website:",
|
|
20
20
|
"1. STANDARD: direkt am Server-Entwurf arbeiten. Ablauf: site_status (Stand, draftRev, wer zuletzt geändert hat) → Änderung mit "
|
|
21
21
|
+ "site_edit_text (Text suchen und ersetzen über alle Seiten) bzw. site_file_get/site_file_put (HTML/CSS) → site_status (Kontrolle). "
|
|
@@ -32,12 +32,19 @@ export const SERVER_INSTRUCTIONS = [
|
|
|
32
32
|
"4. Konflikt (409, conflict:true): nicht erzwingen. Erst site_status aufrufen (zeigt, wer was geändert hat), Inhalt neu holen, Änderung "
|
|
33
33
|
+ "neu anwenden und mit der neuen draftRev erneut senden.",
|
|
34
34
|
"5a. Bilder: Für neue Bilder media_upload_begin → Upload-Link. Wenn du Dateien lesen kannst, lade die ORIGINALDATEI selbst per HTTP PUT "
|
|
35
|
-
+ "hoch
|
|
35
|
+
+ "hoch; sonst gib den Link (uploadPage) an den Nutzer weiter – er lädt am Handy/PC hoch. Danach "
|
|
36
36
|
+ "media_upload_commit und mit site_image_set einsetzen. Nie Varianten selbst bauen, nie ZIP für Medien.",
|
|
37
|
+
...(mode === "local" ? ["5a-lokal. Wenn du einen lokalen Dateipfad hast, nutze media_upload_file; sonst Upload-Link (media_upload_begin). "
|
|
38
|
+
+ "media_upload_file prüft, lädt hoch und übernimmt in einem Schritt und liefert die mediaId."] : []),
|
|
37
39
|
"5b. Design = CSS-Datei ändern (site_file_get/site_file_put), nicht HTML umschreiben. Nach Design- oder Bild-Änderungen "
|
|
38
40
|
+ "site_preview_screenshot (1280 bzw. 390) prüfen, BEVOR du dem Nutzer antwortest.",
|
|
41
|
+
"5c. Bei „rückgängig“ site_undo (bzw. „doch wieder“ site_redo), nie Dateien manuell zurückbauen. Medien löschen nur mit site_media_delete "
|
|
42
|
+
+ "(Papierkorb, 30 Tage; zurückholen mit site_media_restore).",
|
|
39
43
|
"5. Jede Änderung lässt sich rückgängig machen; Rechtstexte (Impressum/Datenschutz) nicht umformulieren – sie kommen aus den Firmendaten.",
|
|
40
|
-
]
|
|
44
|
+
];
|
|
45
|
+
/** Server-Instructions je Modus ("local" = stdio-Paket mit Dateizugriff, "remote" = /mcp). */
|
|
46
|
+
export const instructionsFor = mode => instructionLines(mode).join("\n");
|
|
47
|
+
export const SERVER_INSTRUCTIONS = instructionsFor("remote");
|
|
41
48
|
|
|
42
49
|
const asText = obj => ({ content: [{ type: "text", text: JSON.stringify(obj, null, 2) }] });
|
|
43
50
|
const asError = e => ({ isError: true, content: [{ type: "text", text: "Fehler: " + (e?.message || String(e)) }] });
|
|
@@ -176,14 +183,24 @@ export function registerTools(server, { z, cfg, mode = "local", remote = null })
|
|
|
176
183
|
"SCHREIBEND (Schritt 1 von 2) – neues Bild oder Dokument hochladen: liefert einen UPLOAD-LINK (15 Minuten, einmalig, Größe nach "
|
|
177
184
|
+ "Tarif-Kontingent; Bilder JPG/PNG/WebP/SVG und PDF – PDFs mit Skripten, Öffnen-/Ereignis-Aktionen, Formular-Versand oder "
|
|
178
185
|
+ "Anhängen werden abgelehnt; Office-Dokumente folgen mit dem Virenscanner, bitte als PDF exportieren). Zwei Wege über DENSELBEN Link: (a) Du kannst Dateien lesen → Originaldatei per HTTP PUT an uploadUrl senden "
|
|
179
|
-
+ (mode === "local" ? "(hier
|
|
186
|
+
+ (mode === "local" ? "(hier stattdessen media_upload_file mit dem Dateipfad nutzen) " : "(z. B. curl -T bild.jpg '<uploadUrl>') ")
|
|
180
187
|
+ "– keine Varianten bauen, kein ZIP. (b) Sonst den Link (uploadPage) dem Nutzer geben: er öffnet ihn am Handy/PC, fotografiert oder "
|
|
181
188
|
+ "wählt eine Datei. Danach media_upload_commit.",
|
|
182
189
|
{ site: z.string().optional().describe(SITE_DESC),
|
|
183
190
|
purpose: z.string().optional().describe("Wofür das Bild ist (z. B. 'Titelbild Startseite') – erscheint auf der Upload-Seite nicht, hilft im Verlauf."),
|
|
184
|
-
filename: z.string().optional().describe("Gewünschter Dateiname (z. B. 'team-foto.jpg')."),
|
|
185
|
-
|
|
186
|
-
|
|
191
|
+
filename: z.string().optional().describe("Gewünschter Dateiname (z. B. 'team-foto.jpg').") },
|
|
192
|
+
async ({ site, purpose, filename }) => { try { return asText(await apiMediaUploadBegin(cfg, { site, purpose, filename })); } catch (e) { return asError(e); } }
|
|
193
|
+
);
|
|
194
|
+
if (mode === "local") server.tool(
|
|
195
|
+
"media_upload_file",
|
|
196
|
+
"SCHREIBEND – lokale Datei in EINEM Schritt hochladen (Upload-Link + Datei senden + übernehmen) und die mediaId zurückgeben. "
|
|
197
|
+
+ "Nur Dateien unterhalb des Benutzerordners (Home), keine Symlinks, keine versteckten Ordner; JPG/PNG/WebP/SVG oder PDF, max. 15 MB "
|
|
198
|
+
+ "(Prüfung VOR dem Senden – abgelehnte Dateien verlassen den Rechner nicht). Danach Bilder mit site_image_set einsetzen, "
|
|
199
|
+
+ "Dokumente als Download verlinken (href = mediaId). Ohne lokalen Pfad: media_upload_begin (Upload-Link für den Nutzer).",
|
|
200
|
+
{ site: z.string().optional().describe(SITE_DESC),
|
|
201
|
+
path: z.string().describe("Absoluter Dateipfad unterhalb des Home-Ordners (z. B. ~/Bilder/team.jpg)."),
|
|
202
|
+
purpose: z.string().optional().describe("Wofür die Datei ist (z. B. 'Titelbild Startseite') – hilft im Verlauf.") },
|
|
203
|
+
async ({ site, path, purpose }) => { try { return asText(await apiMediaUploadFile(cfg, { site, path, purpose })); } catch (e) { return asError(e); } }
|
|
187
204
|
);
|
|
188
205
|
server.tool(
|
|
189
206
|
"media_upload_commit",
|
|
@@ -195,7 +212,7 @@ export function registerTools(server, { z, cfg, mode = "local", remote = null })
|
|
|
195
212
|
);
|
|
196
213
|
server.tool(
|
|
197
214
|
"site_media_list",
|
|
198
|
-
"LESEND: Medien der Website (mediaId, Name, Typ, Größe, Maße; Uploads und Bilder der importierten Website) und das Medien-Kontingent.",
|
|
215
|
+
"LESEND: Medien der Website (mediaId, Name, Typ, Größe, Maße; Uploads und Bilder der importierten Website) und das Medien-Kontingent; trash = Papierkorb (trashId für site_media_restore).",
|
|
199
216
|
{ site: z.string().optional().describe(SITE_DESC) },
|
|
200
217
|
async ({ site }) => { try { return asText(await apiMediaList(cfg, { site })); } catch (e) { return asError(e); } }
|
|
201
218
|
);
|
|
@@ -209,10 +226,42 @@ export function registerTools(server, { z, cfg, mode = "local", remote = null })
|
|
|
209
226
|
baseRev: z.number().int().optional().describe("Optional: draftRev (Konfliktschutz).") },
|
|
210
227
|
async (a) => { try { return asText(await apiImageSet(cfg, a)); } catch (e) { return asError(e); } }
|
|
211
228
|
);
|
|
229
|
+
server.tool(
|
|
230
|
+
"site_undo",
|
|
231
|
+
"SCHREIBEND (Entwurf): macht die letzte(n) Änderung(en) rückgängig – derselbe Verlauf wie der Rückgängig-Knopf im Dashboard (auch "
|
|
232
|
+
+ "Datei-Änderungen, Bildtausch, Texte). Bei „rückgängig“ IMMER dieses Tool, nie Dateien von Hand zurückbauen. Antwort nennt je Schritt "
|
|
233
|
+
+ "Op-Typ, Seite(n) und Akteur (vonAnderem:true = Änderung stammte von jemand anderem → dem Nutzer sagen). Nicht live bis site_publish.",
|
|
234
|
+
{ site: z.string().optional().describe(SITE_DESC), steps: z.number().int().min(1).max(10).default(1).describe("Anzahl Schritte (1–10)."),
|
|
235
|
+
baseRev: z.number().int().optional().describe("Optional: draftRev aus site_status (409, falls inzwischen jemand anderes geändert hat).") },
|
|
236
|
+
async ({ site, steps, baseRev }) => { try { return asText(await apiUndo(cfg, { site, steps, baseRev })); } catch (e) { return asError(e); } }
|
|
237
|
+
);
|
|
238
|
+
server.tool(
|
|
239
|
+
"site_redo",
|
|
240
|
+
"SCHREIBEND (Entwurf): stellt zuletzt rückgängig gemachte Änderung(en) wieder her (Gegenstück zu site_undo; nur bis zur nächsten neuen Änderung möglich).",
|
|
241
|
+
{ site: z.string().optional().describe(SITE_DESC), steps: z.number().int().min(1).max(10).default(1).describe("Anzahl Schritte (1–10)."),
|
|
242
|
+
baseRev: z.number().int().optional().describe("Optional: draftRev (Konfliktschutz).") },
|
|
243
|
+
async ({ site, steps, baseRev }) => { try { return asText(await apiUndo(cfg, { site, steps, baseRev, redo: true })); } catch (e) { return asError(e); } }
|
|
244
|
+
);
|
|
245
|
+
server.tool(
|
|
246
|
+
"site_media_delete",
|
|
247
|
+
"SCHREIBEND: verschiebt ein Medium (mediaId aus site_media_list) in den PAPIERKORB – 30 Tage wiederherstellbar (site_media_restore), "
|
|
248
|
+
+ "nie endgültig. Wird es noch verwendet (Entwurf ODER veröffentlichter Stand), kommt inUse:true mit den Fundstellen und es wird NICHT "
|
|
249
|
+
+ "gelöscht – erst ersetzen; force:true nur auf ausdrücklichen Wunsch des Nutzers (nur eigene Uploads; Import-Bilder nur, wenn unbenutzt).",
|
|
250
|
+
{ site: z.string().optional().describe(SITE_DESC), mediaId: z.string().describe("mediaId aus site_media_list (z. B. media/team.webp)."),
|
|
251
|
+
force: z.boolean().optional().describe("Trotz Verwendung in den Papierkorb (nur nach Rückfrage beim Nutzer).") },
|
|
252
|
+
async ({ site, mediaId, force }) => { try { return asText(await apiMediaDelete(cfg, { site, mediaId, force })); } catch (e) { return asError(e); } }
|
|
253
|
+
);
|
|
254
|
+
server.tool(
|
|
255
|
+
"site_media_restore",
|
|
256
|
+
"SCHREIBEND: holt ein Medium aus dem Papierkorb zurück (trashId aus site_media_delete bzw. site_media_list → trash). Ist der Name "
|
|
257
|
+
+ "inzwischen vergeben, kommt es als „…-wiederhergestellt“ zurück.",
|
|
258
|
+
{ site: z.string().optional().describe(SITE_DESC), trashId: z.string().describe("trashId aus site_media_delete bzw. site_media_list.") },
|
|
259
|
+
async ({ site, trashId }) => { try { return asText(await apiMediaRestore(cfg, { site, trashId })); } catch (e) { return asError(e); } }
|
|
260
|
+
);
|
|
212
261
|
server.tool(
|
|
213
262
|
"site_preview_screenshot",
|
|
214
263
|
"LESEND: Bildschirmfoto des ENTWURFS einer Seite als Bild (Desktop width=1280, Smartphone width=390; max. 4000 px hoch, ohne "
|
|
215
|
-
+ "JavaScript-
|
|
264
|
+
+ "JavaScript; Einblend-Animationen werden sichtbar dargestellt). Nach Design- oder Bild-Änderungen prüfen, bevor du antwortest. Begrenzt (30 je Stunde, ein Lauf gleichzeitig).",
|
|
216
265
|
{ site: z.string().optional().describe(SITE_DESC), page: z.string().default("index.html").describe("Seite (ID, Datei oder Slug)."),
|
|
217
266
|
width: z.union([z.literal(1280), z.literal(390)]).default(1280) },
|
|
218
267
|
async ({ site, page, width }) => {
|