@prontosite/mcp 0.12.1 → 0.13.2
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 +26 -0
- package/api.mjs +64 -0
- package/index.mjs +1 -1
- package/package.json +1 -1
- package/tools.mjs +99 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,32 @@
|
|
|
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.2 – 2026-10-19
|
|
7
|
+
|
|
8
|
+
- Upload-Beschreibung: Bilder und PDF; PDFs mit Skripten, automatischen Aktionen, Formular-Versand oder Anhängen werden abgelehnt;
|
|
9
|
+
Office-Dokumente folgen mit dem Virenscanner (bitte als PDF exportieren).
|
|
10
|
+
|
|
11
|
+
## 0.13.1 – 2026-10-19
|
|
12
|
+
|
|
13
|
+
- `media_upload_begin`/`media_upload_commit`: Dokumente (PDF, DOCX, XLSX, PPTX, ODT) werden jetzt angenommen – nach derselben
|
|
14
|
+
Strukturprüfung wie im Dashboard (keine Skripte, Makros, Öffnen-Aktionen, eingebetteten Dateien). Dokumente als Download verlinken.
|
|
15
|
+
|
|
16
|
+
## 0.13.0 – 2026-10-19
|
|
17
|
+
|
|
18
|
+
- **Bilder hochladen und einsetzen:** `media_upload_begin` (ein Link, zwei Wege: Datei per PUT selbst hochladen – im lokalen Paket
|
|
19
|
+
einfach `path` angeben – oder Link an den Nutzer, Upload-Seite mit Kamera), `media_upload_commit` (Prüfung, Kontingent,
|
|
20
|
+
Optimierung auf dem Server → `mediaId`), `site_media_list`, `site_image_set` (Bild per Knoten-ID oder Marker tauschen, mit `alt`).
|
|
21
|
+
- **Design-Sicht:** `site_preview_screenshot` liefert den Entwurf als Bild (1280/390 px), meldet horizontalen Überlauf.
|
|
22
|
+
- Instructions: Bilder nie als Varianten/ZIP, Design über die CSS-Datei, nach Design-Änderungen Screenshot prüfen.
|
|
23
|
+
|
|
24
|
+
## 0.12.2 – 2026-10-19
|
|
25
|
+
|
|
26
|
+
- **Textarbeit ohne HTML (Übersetzen, Umformulieren):** neu `site_page_texts` (alle Texte einer Seite inkl. Menü-/Button-Texten,
|
|
27
|
+
`alt`/`title`, Seitentitel, Meta-Beschreibung), `site_page_texts_set` (alles in einem Aufruf, ein Rückgängig-Schritt, unbekannte
|
|
28
|
+
IDs → nichts geändert) und `site_page_copy` (Seite duplizieren, z. B. `index.html` → `en/index.html`, Pfade angepasst,
|
|
29
|
+
Sprachfassung verknüpft). Rechtstext-Seiten ausgenommen.
|
|
30
|
+
- Instructions: Übersetzung/Textüberarbeitung je Seite über `site_page_texts` → `site_page_texts_set`, nie HTML per `site_file_put`.
|
|
31
|
+
|
|
6
32
|
## 0.12.1 – 2026-10-19
|
|
7
33
|
|
|
8
34
|
- **`site_edit_text` in einem Aufruf für die ganze Website:** `scope:"all"` ersetzt alle Fundstellen auf allen Seiten sofort
|
package/api.mjs
CHANGED
|
@@ -397,6 +397,70 @@ export async function apiEditText(cfg, { site, find, replace, scope = null, regi
|
|
|
397
397
|
method: "POST", headers: { ...authHeaders(token, cfg), "content-type": "application/json" }, body: JSON.stringify(body) }));
|
|
398
398
|
}
|
|
399
399
|
|
|
400
|
+
/** GET /api/page-texts/<site>?page= – alle Texte einer Seite (Textknoten, reine Text-Links, Attribute, Titel/Beschreibung). */
|
|
401
|
+
export async function apiPageTexts(cfg, { site, page } = {}, fetchImpl = fetch) {
|
|
402
|
+
const { token, slug } = resolveTarget(cfg, site);
|
|
403
|
+
return asJson(await fetchImpl(`${baseUrl(cfg)}/page-texts/${encodeURIComponent(slug)}?page=${encodeURIComponent(page ?? "")}`, { headers: authHeaders(token, cfg) }));
|
|
404
|
+
}
|
|
405
|
+
/** POST /api/page-texts/<site> – Texte einer Seite in EINEM Aufruf setzen (ein Undo-Eintrag). 409 kommt als Objekt zurück. */
|
|
406
|
+
export async function apiPageTextsSet(cfg, { site, page, texts, baseRev } = {}, fetchImpl = fetch) {
|
|
407
|
+
const { token, slug } = resolveTarget(cfg, site);
|
|
408
|
+
return asJsonOr409(await fetchImpl(`${baseUrl(cfg)}/page-texts/${encodeURIComponent(slug)}`, {
|
|
409
|
+
method: "POST", headers: { ...authHeaders(token, cfg), "content-type": "application/json" }, body: JSON.stringify({ page, texts, baseRev }) }));
|
|
410
|
+
}
|
|
411
|
+
/** POST /api/page-copy/<site> – Seite duplizieren (z. B. index.html → en/index.html), relative Pfade angepasst. */
|
|
412
|
+
export async function apiPageCopy(cfg, { site, from, to, baseRev = null } = {}, fetchImpl = fetch) {
|
|
413
|
+
const { token, slug } = resolveTarget(cfg, site);
|
|
414
|
+
return asJsonOr409(await fetchImpl(`${baseUrl(cfg)}/page-copy/${encodeURIComponent(slug)}`, {
|
|
415
|
+
method: "POST", headers: { ...authHeaders(token, cfg), "content-type": "application/json" }, body: JSON.stringify({ from, to, ...(Number.isInteger(baseRev) ? { baseRev } : {}) }) }));
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/** POST /api/media-upload/<site> – Upload-Link (15 Min., einmalig) für ein neues Bild; lokal optional gleich hochladen (path). */
|
|
419
|
+
export async function apiMediaUploadBegin(cfg, { site, purpose = null, filename = null, path: localPath = null } = {}, fetchImpl = fetch) {
|
|
420
|
+
const { token, slug } = resolveTarget(cfg, site);
|
|
421
|
+
const name = filename || (localPath ? path.basename(localPath) : null);
|
|
422
|
+
const out = await asJson(await fetchImpl(`${baseUrl(cfg)}/media-upload/${encodeURIComponent(slug)}`, {
|
|
423
|
+
method: "POST", headers: { ...authHeaders(token, cfg), "content-type": "application/json" }, body: JSON.stringify({ purpose, filename: name }) }));
|
|
424
|
+
if (localPath) { // lokales Paket mit Dateizugriff: Originaldatei selbst per PUT hochladen (keine Varianten, kein ZIP)
|
|
425
|
+
if (!fs.existsSync(localPath)) throw new Error(`Datei nicht gefunden: ${localPath}`);
|
|
426
|
+
const up = await fetchImpl(out.uploadUrl, { method: "PUT", headers: { "x-filename": encodeURIComponent(name || "bild") }, body: fs.readFileSync(localPath) });
|
|
427
|
+
const r = await up.json().catch(() => ({}));
|
|
428
|
+
if (!up.ok) throw new Error(r.error || `Upload fehlgeschlagen (HTTP ${up.status}).`);
|
|
429
|
+
return { ...out, uploaded: true, bytes: r.bytes, next: "Hochgeladen – jetzt media_upload_commit {uploadId}." };
|
|
430
|
+
}
|
|
431
|
+
return out;
|
|
432
|
+
}
|
|
433
|
+
/** POST /api/media-commit/<site> – hochgeladene Datei prüfen und als Medium ablegen → mediaId. */
|
|
434
|
+
export async function apiMediaCommit(cfg, { site, uploadId } = {}, fetchImpl = fetch) {
|
|
435
|
+
const { token, slug } = resolveTarget(cfg, site);
|
|
436
|
+
const res = await fetchImpl(`${baseUrl(cfg)}/media-commit/${encodeURIComponent(slug)}`, {
|
|
437
|
+
method: "POST", headers: { ...authHeaders(token, cfg), "content-type": "application/json" }, body: JSON.stringify({ uploadId }) });
|
|
438
|
+
if (res.status === 409) { const b = await res.json().catch(() => ({})); if (b.pending) return { pending: true, hinweis: b.error }; throw new Error(b.error || "Konflikt (409)."); }
|
|
439
|
+
return asJson(res);
|
|
440
|
+
}
|
|
441
|
+
/** GET /api/media/<site> – Medien der Website (mediaId, Typ, Größe, Maße) + Kontingent. */
|
|
442
|
+
export async function apiMediaList(cfg, { site } = {}, fetchImpl = fetch) {
|
|
443
|
+
const { token, slug } = resolveTarget(cfg, site);
|
|
444
|
+
return asJson(await fetchImpl(`${baseUrl(cfg)}/media/${encodeURIComponent(slug)}`, { headers: authHeaders(token, cfg) }));
|
|
445
|
+
}
|
|
446
|
+
/** POST /api/image-set/<site> – Bild einer Seite tauschen (setImage, ein Undo-Schritt). */
|
|
447
|
+
export async function apiImageSet(cfg, { site, page, nodeId = null, marker = null, mediaId, alt = null, baseRev = null } = {}, fetchImpl = fetch) {
|
|
448
|
+
const { token, slug } = resolveTarget(cfg, site);
|
|
449
|
+
return asJsonOr409(await fetchImpl(`${baseUrl(cfg)}/image-set/${encodeURIComponent(slug)}`, {
|
|
450
|
+
method: "POST", headers: { ...authHeaders(token, cfg), "content-type": "application/json" },
|
|
451
|
+
body: JSON.stringify({ page, mediaId, ...(nodeId ? { nodeId } : {}), ...(marker ? { marker } : {}), ...(alt != null ? { alt } : {}), ...(Number.isInteger(baseRev) ? { baseRev } : {}) }) }));
|
|
452
|
+
}
|
|
453
|
+
/** GET /api/preview-screenshot/<site>?page=&width= – PNG des Entwurfs (Base64 + Maße). */
|
|
454
|
+
export async function apiPreviewScreenshot(cfg, { site, page = "index.html", width = 1280 } = {}, fetchImpl = fetch) {
|
|
455
|
+
const { token, slug } = resolveTarget(cfg, site);
|
|
456
|
+
const res = await fetchImpl(`${baseUrl(cfg)}/preview-screenshot/${encodeURIComponent(slug)}?page=${encodeURIComponent(page)}&width=${encodeURIComponent(width)}`, { headers: authHeaders(token, cfg) });
|
|
457
|
+
if (!res.ok) return asJson(res);
|
|
458
|
+
const buf = Buffer.from(await res.arrayBuffer());
|
|
459
|
+
const h = k => res.headers.get(k);
|
|
460
|
+
return { png: buf.toString("base64"), width: Number(h("x-ps-width")), height: Number(h("x-ps-height")), fullHeight: Number(h("x-ps-full-height")),
|
|
461
|
+
truncated: h("x-ps-truncated") === "1", overflowX: Number(h("x-ps-overflow-x") || 0), ms: Number(h("x-ps-ms")), page: decodeURIComponent(h("x-ps-page") || page), bytes: buf.length };
|
|
462
|
+
}
|
|
463
|
+
|
|
400
464
|
/** GET/POST /api/lock/<site> – sanfte Sperre („in Bearbeitung durch …"); blockiert niemanden. action: lock|unlock|takeover. */
|
|
401
465
|
export async function apiLock(cfg, { site, action = null, note = null } = {}, fetchImpl = fetch) {
|
|
402
466
|
const { token, slug } = resolveTarget(cfg, site);
|
package/index.mjs
CHANGED
|
@@ -21,7 +21,7 @@ const cfg = loadConfig();
|
|
|
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.
|
|
24
|
+
const server = new McpServer({ name: "prontosite", version: "0.13.2" }, { instructions: SERVER_INSTRUCTIONS });
|
|
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.
|
|
3
|
+
"version": "0.13.2",
|
|
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,7 +9,7 @@
|
|
|
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, 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, 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";
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* Arbeitsweise (MCP-`instructions` beim Initialize – lokal UND /mcp): Weg B ist der Standard (Nick 19.10.).
|
|
@@ -25,9 +25,17 @@ export const SERVER_INSTRUCTIONS = [
|
|
|
25
25
|
"2a. Für Text-, Link- und Kontaktdaten-Änderungen (Telefonnummer, E-Mail, Adresse, Öffnungszeiten) IMMER site_edit_text mit "
|
|
26
26
|
+ "scope:\"all\" (bei Bedarf region:\"footer\"/\"header\") – EIN Aufruf für alle Seiten, inkl. tel:/mailto:-Links. NIE Seite für Seite per "
|
|
27
27
|
+ "site_file_put. Meldet die Antwort company, zusätzlich site_company_set für die Firmendaten (Impressum/Datenschutz/Kontakt).",
|
|
28
|
+
"2b. Für Übersetzungen und Textüberarbeitung: site_page_texts → Texte übersetzen/überarbeiten → site_page_texts_set, JE SEITE ein Aufruf "
|
|
29
|
+
+ "(neue Sprachfassung vorher mit site_page_copy, z. B. index.html → en/index.html). NIE HTML über site_file_put umschreiben. "
|
|
30
|
+
+ "Rechtstext-Seiten (Impressum/Datenschutz) sind ausgenommen.",
|
|
28
31
|
"3. Veröffentlichen (site_publish, publish=true) NUR nach ausdrücklicher Bestätigung des Nutzers im Chat – bis dahin bleibt alles im Entwurf.",
|
|
29
32
|
"4. Konflikt (409, conflict:true): nicht erzwingen. Erst site_status aufrufen (zeigt, wer was geändert hat), Inhalt neu holen, Änderung "
|
|
30
33
|
+ "neu anwenden und mit der neuen draftRev erneut senden.",
|
|
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 (lokales Paket: path angeben); sonst gib den Link (uploadPage) an den Nutzer weiter – er lädt am Handy/PC hoch. Danach "
|
|
36
|
+
+ "media_upload_commit und mit site_image_set einsetzen. Nie Varianten selbst bauen, nie ZIP für Medien.",
|
|
37
|
+
"5b. Design = CSS-Datei ändern (site_file_get/site_file_put), nicht HTML umschreiben. Nach Design- oder Bild-Änderungen "
|
|
38
|
+
+ "site_preview_screenshot (1280 bzw. 390) prüfen, BEVOR du dem Nutzer antwortest.",
|
|
31
39
|
"5. Jede Änderung lässt sich rückgängig machen; Rechtstexte (Impressum/Datenschutz) nicht umformulieren – sie kommen aus den Firmendaten.",
|
|
32
40
|
].join("\n");
|
|
33
41
|
|
|
@@ -128,6 +136,96 @@ export function registerTools(server, { z, cfg, mode = "local", remote = null })
|
|
|
128
136
|
async ({ site, find, replace, scope, region, attributes, pages, baseRev }) => { try { return asText(await apiEditText(cfg, { site, find, replace, scope, region, attributes, pages, baseRev })); } catch (e) { return asError(e); } }
|
|
129
137
|
);
|
|
130
138
|
|
|
139
|
+
server.tool(
|
|
140
|
+
"site_page_texts",
|
|
141
|
+
"LESEND – STANDARDWEG für Übersetzung/Umformulierung: liefert ALLE Texte einer Seite ohne HTML: [{id, role, text, markdown?}] – "
|
|
142
|
+
+ "Textknoten, reine Text-Links (Menüpunkte, Buttons), Attribute (alt, title, aria-label, placeholder) sowie Seitentitel (head:title) "
|
|
143
|
+
+ "und Meta-Beschreibung (head:description), dazu draftRev. Danach Texte bearbeiten und mit site_page_texts_set in EINEM Aufruf "
|
|
144
|
+
+ "zurückschreiben. Rechtstext-Seiten liefern 403 (ausgenommen).",
|
|
145
|
+
{ site: z.string().optional().describe(SITE_DESC),
|
|
146
|
+
page: z.string().describe("Seite: Seiten-ID, Datei (z. B. 'index.html', 'en/index.html') oder Slug.") },
|
|
147
|
+
async ({ site, page }) => { try { return asText(await apiPageTexts(cfg, { site, page })); } catch (e) { return asError(e); } }
|
|
148
|
+
);
|
|
149
|
+
server.tool(
|
|
150
|
+
"site_page_texts_set",
|
|
151
|
+
"SCHREIBEND (Entwurf, nicht live) – STANDARDWEG für Übersetzung/Umformulierung: setzt Texte einer Seite in EINEM Aufruf "
|
|
152
|
+
+ "(ein Rückgängig-Schritt). texts = [{id, text}] mit IDs aus site_page_texts (bei Einträgen mit markdown: markdown zurückgeben, "
|
|
153
|
+
+ "dann bleiben Fett/Kursiv/Links erhalten). Struktur, Klassen und Linkziele bleiben unberührt. Unbekannte IDs → Fehler, es wird "
|
|
154
|
+
+ "NICHTS geändert (keine Teilanwendung). baseRev = draftRev aus site_page_texts; bei 409 erst site_status/site_page_texts neu.",
|
|
155
|
+
{ site: z.string().optional().describe(SITE_DESC),
|
|
156
|
+
page: z.string().describe("Seite (wie bei site_page_texts)."),
|
|
157
|
+
texts: z.array(z.object({ id: z.string(), text: z.string().optional(), markdown: z.string().optional() })).describe("Neue Texte je ID (nur geänderte nötig)."),
|
|
158
|
+
baseRev: z.number().int().describe("draftRev aus site_page_texts (Konfliktschutz).") },
|
|
159
|
+
async ({ site, page, texts, baseRev }) => { try { return asText(await apiPageTextsSet(cfg, { site, page, texts, baseRev })); } catch (e) { return asError(e); } }
|
|
160
|
+
);
|
|
161
|
+
server.tool(
|
|
162
|
+
"site_page_copy",
|
|
163
|
+
"SCHREIBEND (Entwurf) – neue Sprachfassung oder Variante einer Seite: dupliziert eine Seite (inkl. Dashboard-Änderungen) unter neuem "
|
|
164
|
+
+ "Pfad, z. B. from='index.html', to='en/index.html'. Relative Pfade (Bilder, CSS, Links) werden angepasst; in einem Sprachordner "
|
|
165
|
+
+ "setzt der Server <html lang> und verknüpft die Kopie als Sprachfassung. Danach site_page_texts → übersetzen → site_page_texts_set "
|
|
166
|
+
+ "auf der Kopie. Vorhandene Dateien werden nie überschrieben; Rechtstext-Seiten werden nicht kopiert.",
|
|
167
|
+
{ site: z.string().optional().describe(SITE_DESC),
|
|
168
|
+
from: z.string().describe("Quellseite (Seiten-ID, Datei oder Slug)."),
|
|
169
|
+
to: z.string().describe("Neuer Pfad der Kopie, z. B. 'en/index.html' oder 'en/kontakt/index.html'."),
|
|
170
|
+
baseRev: z.number().int().optional().describe("Optional: draftRev aus site_status (Konfliktschutz).") },
|
|
171
|
+
async ({ site, from, to, baseRev }) => { try { return asText(await apiPageCopy(cfg, { site, from, to, baseRev })); } catch (e) { return asError(e); } }
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
server.tool(
|
|
175
|
+
"media_upload_begin",
|
|
176
|
+
"SCHREIBEND (Schritt 1 von 2) – neues Bild oder Dokument hochladen: liefert einen UPLOAD-LINK (15 Minuten, einmalig, Größe nach "
|
|
177
|
+
+ "Tarif-Kontingent; Bilder JPG/PNG/WebP/SVG und PDF – PDFs mit Skripten, Öffnen-/Ereignis-Aktionen, Formular-Versand oder "
|
|
178
|
+
+ "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 einfach path angeben, das Paket lädt selbst hoch) " : "(z. B. curl -T bild.jpg '<uploadUrl>') ")
|
|
180
|
+
+ "– keine Varianten bauen, kein ZIP. (b) Sonst den Link (uploadPage) dem Nutzer geben: er öffnet ihn am Handy/PC, fotografiert oder "
|
|
181
|
+
+ "wählt eine Datei. Danach media_upload_commit.",
|
|
182
|
+
{ site: z.string().optional().describe(SITE_DESC),
|
|
183
|
+
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
|
+
...(mode === "local" ? { path: z.string().optional().describe("Lokaler Dateipfad – das Paket lädt die Datei direkt hoch.") } : {}) },
|
|
186
|
+
async ({ site, purpose, filename, path }) => { try { return asText(await apiMediaUploadBegin(cfg, { site, purpose, filename, path: mode === "local" ? path : null })); } catch (e) { return asError(e); } }
|
|
187
|
+
);
|
|
188
|
+
server.tool(
|
|
189
|
+
"media_upload_commit",
|
|
190
|
+
"SCHREIBEND (Schritt 2 von 2): übernimmt die hochgeladene Datei – Prüfung von Typ/Inhalt, Kontingent und Virenscan, Optimierung "
|
|
191
|
+
+ "(WebP, Varianten, EXIF entfernt) – und liefert die mediaId. Antwort pending:true = der Nutzer hat noch nicht hochgeladen (kurz warten "
|
|
192
|
+
+ "bzw. nachfragen). Bilder danach mit site_image_set einsetzen; Dokumente als Download verlinken (href = mediaId).",
|
|
193
|
+
{ site: z.string().optional().describe(SITE_DESC), uploadId: z.string().describe("uploadId aus media_upload_begin.") },
|
|
194
|
+
async ({ site, uploadId }) => { try { return asText(await apiMediaCommit(cfg, { site, uploadId })); } catch (e) { return asError(e); } }
|
|
195
|
+
);
|
|
196
|
+
server.tool(
|
|
197
|
+
"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.",
|
|
199
|
+
{ site: z.string().optional().describe(SITE_DESC) },
|
|
200
|
+
async ({ site }) => { try { return asText(await apiMediaList(cfg, { site })); } catch (e) { return asError(e); } }
|
|
201
|
+
);
|
|
202
|
+
server.tool(
|
|
203
|
+
"site_image_set",
|
|
204
|
+
"SCHREIBEND (Entwurf): setzt ein Bild auf einer Seite ein (ein Rückgängig-Schritt). Ziel: nodeId des Bild-Knotens oder marker (id- bzw. "
|
|
205
|
+
+ "data-ps-Wert eines Elements; dessen Bild wird getauscht). mediaId aus media_upload_commit oder site_media_list. alt = Bildbeschreibung "
|
|
206
|
+
+ "(für Barrierefreiheit/SEO immer setzen). Danach site_preview_screenshot prüfen.",
|
|
207
|
+
{ site: z.string().optional().describe(SITE_DESC), page: z.string().describe("Seite (ID, Datei oder Slug)."),
|
|
208
|
+
nodeId: z.string().optional(), marker: z.string().optional(), mediaId: z.string(), alt: z.string().optional(),
|
|
209
|
+
baseRev: z.number().int().optional().describe("Optional: draftRev (Konfliktschutz).") },
|
|
210
|
+
async (a) => { try { return asText(await apiImageSet(cfg, a)); } catch (e) { return asError(e); } }
|
|
211
|
+
);
|
|
212
|
+
server.tool(
|
|
213
|
+
"site_preview_screenshot",
|
|
214
|
+
"LESEND: Bildschirmfoto des ENTWURFS einer Seite als Bild (Desktop width=1280, Smartphone width=390; max. 4000 px hoch, ohne "
|
|
215
|
+
+ "JavaScript-Effekte). Nach Design- oder Bild-Änderungen prüfen, bevor du antwortest. Begrenzt (30 je Stunde, ein Lauf gleichzeitig).",
|
|
216
|
+
{ site: z.string().optional().describe(SITE_DESC), page: z.string().default("index.html").describe("Seite (ID, Datei oder Slug)."),
|
|
217
|
+
width: z.union([z.literal(1280), z.literal(390)]).default(1280) },
|
|
218
|
+
async ({ site, page, width }) => {
|
|
219
|
+
try {
|
|
220
|
+
const r = await apiPreviewScreenshot(cfg, { site, page, width });
|
|
221
|
+
if (!r.png) return asText(r);
|
|
222
|
+
return { content: [{ type: "image", data: r.png, mimeType: "image/png" },
|
|
223
|
+
{ type: "text", text: JSON.stringify({ page: r.page, width: r.width, height: r.height, ...(r.truncated ? { abgeschnitten: `Seite ist ${r.fullHeight} px hoch – gezeigt die oberen ${r.height} px` } : {}),
|
|
224
|
+
...(r.overflowX ? { horizontalerUeberlauf: `Inhalt ist ${r.overflowX} px breit (Viewport ${r.width}) – die Seite scrollt seitwärts; meist ein Bild/Element ohne max-width.` } : {}), ms: r.ms }) }] };
|
|
225
|
+
} catch (e) { return asError(e); }
|
|
226
|
+
}
|
|
227
|
+
);
|
|
228
|
+
|
|
131
229
|
if (mode === "local") {
|
|
132
230
|
server.tool(
|
|
133
231
|
"site_export",
|