@prontosite/mcp 0.13.7 → 0.14.1

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 CHANGED
@@ -3,6 +3,33 @@
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.14.1 – 2026-10-22
7
+
8
+ - Instructions + `media_search`: Fotos nie per URL von Unsplash oder anderen Bilddiensten (images.unsplash.com, imgix …) ins
9
+ HTML/CSS schreiben – sonst lädt jeder Besucher das Bild beim Dienst (Besucher-IPs in die USA, Einwilligungspflicht); immer
10
+ `media_import`, ProntoSite liefert das Foto selbst aus.
11
+
12
+ ## 0.14.0 – 2026-10-22
13
+
14
+ Enthält 0.13.8 (nie einzeln veröffentlicht).
15
+ - Neu: `media_search {site, query, orientation?, count≤6}` – Unsplash-Fotos suchen; je Treffer eine kleine Vorschau als Bild,
16
+ Fotograf, Lizenz-Hinweis und `resultId` (1 h gültig). Gleiche Suchen kommen 1 h aus dem Cache.
17
+ - Neu: `media_import {site, resultId}` – Treffer übernehmen (Download-Trigger laut Unsplash-Richtlinie, Varianten, Kontingent,
18
+ Fotograf/Quelle in den Medien-Metadaten) → `mediaId` + vorbelegter alt-Text; danach `site_image_set`.
19
+ - Instructions: „Bei ‚passendes Bild für Abschnitt X': site_page_texts → media_search → 2–3 Vorschauen zur Wahl → media_import
20
+ + site_image_set + Screenshot."
21
+
22
+ ## 0.13.8 – 2026-10-21
23
+
24
+ - Neu: `site_edit_html {find, replace, region?, scope?, pages?}` – Markup-Fragment (z. B. Footer) über alle Seiten in EINEM
25
+ Aufruf ersetzen, ein Rückgängig-Schritt; mehrere Fundstellen ohne `scope:"all"` → nur Liste. Rechtstext-Bereich, Skript-Regeln
26
+ und das ProntoSite-Badge geschützt (`skipped`).
27
+ - Badge-Sperre: `site_edit_text`, `site_edit_html` und `site_file_put` ändern, entfernen oder bauen das Badge (`data-ps="badge"`)
28
+ nicht nach – es wird beim Ausliefern erzeugt; Änderungen daneben und das Wiederherstellen des Original-Markers bleiben erlaubt.
29
+ - Rate-Limit (429): Meldung nennt die Rest-Wartezeit und den richtigen Weg (statt allgemeinem Text). Schreib-Limit je Token/Grant
30
+ und Konto: Betreuer 200, Kunden 30 Aufrufe je 10 Minuten.
31
+ - Instructions: Markup über alle Seiten per `site_edit_html`, nie Seite für Seite per `site_file_put`; Badge nie im HTML ändern.
32
+
6
33
  ## 0.13.7 – 2026-10-20
7
34
 
8
35
  - Neu: `site_forms_get` – Konto-Empfänger, je Formular Art (verwaltetes ProntoSite-Formular oder eigenes `<form>` aus dem
package/api.mjs CHANGED
@@ -115,8 +115,8 @@ export function friendlyApiError(status, serverError = "") {
115
115
  return "Dein API-Token ist ungültig oder abgelaufen. Token im Dashboard → API-Zugang neu erstellen und in ~/.prontosite.json eintragen.";
116
116
  if (status === 403 && /API-Zugang nicht im Tarif|Starter-Plan/i.test(raw))
117
117
  return "Dein ProntoSite-Tarif enthält keinen API-Zugang. Wähle im Dashboard → Zahlung einen Tarif oder bitte deinen Betreuer um eine API-Freigabe.";
118
- if (status === 429)
119
- return "Zu viele Anfragen in kurzer Zeit (Rate-Limit). Bitte einen Moment warten und erneut versuchen.";
118
+ if (status === 429) // Server nennt Wartezeit + richtigen Weg → durchreichen; sonst allgemeiner Hinweis
119
+ return /wieder möglich in/.test(raw) ? raw : "Zu viele Anfragen in kurzer Zeit (Rate-Limit). Bitte einen Moment warten und erneut versuchen.";
120
120
  return raw || `HTTP ${status}`;
121
121
  }
122
122
 
@@ -402,6 +402,15 @@ export async function apiEditText(cfg, { site, find, replace, scope = null, regi
402
402
  method: "POST", headers: { ...authHeaders(token, cfg), "content-type": "application/json" }, body: JSON.stringify(body) }));
403
403
  }
404
404
 
405
+ /** POST /api/edit-html/<site> – Markup-Fragment über alle Seiten ersetzen (z. B. Footer), EIN Undo-Eintrag. 409 als Objekt. */
406
+ export async function apiEditHtml(cfg, { site, find, replace, scope = null, region = null, pages = null, baseRev = null } = {}, fetchImpl = fetch) {
407
+ const { token, slug } = resolveTarget(cfg, site);
408
+ const body = { find, replace, ...(scope ? { scope } : {}), ...(region ? { region } : {}),
409
+ ...(Array.isArray(pages) && pages.length ? { pages } : {}), ...(Number.isInteger(baseRev) ? { baseRev } : {}) };
410
+ return asJsonOr409(await fetchImpl(`${baseUrl(cfg)}/edit-html/${encodeURIComponent(slug)}`, {
411
+ method: "POST", headers: { ...authHeaders(token, cfg), "content-type": "application/json" }, body: JSON.stringify(body) }));
412
+ }
413
+
405
414
  /** GET /api/page-texts/<site>?page= – alle Texte einer Seite (Textknoten, reine Text-Links, Attribute, Titel/Beschreibung). */
406
415
  export async function apiPageTexts(cfg, { site, page } = {}, fetchImpl = fetch) {
407
416
  const { token, slug } = resolveTarget(cfg, site);
@@ -551,6 +560,18 @@ export async function apiMediaRestore(cfg, { site, trashId } = {}, fetchImpl = f
551
560
  return asJson(await fetchImpl(`${baseUrl(cfg)}/media-restore/${encodeURIComponent(slug)}`, {
552
561
  method: "POST", headers: { ...authHeaders(token, cfg), "content-type": "application/json" }, body: JSON.stringify({ trashId }) }));
553
562
  }
563
+ /** GET /api/media-search/<site>?query=&orientation=&count= – Unsplash-Treffer (Vorschau base64, Fotograf, resultId). */
564
+ export async function apiMediaSearch(cfg, { site, query, orientation = null, count = 3 } = {}, fetchImpl = fetch) {
565
+ const { token, slug } = resolveTarget(cfg, site);
566
+ const q = new URLSearchParams({ query: String(query ?? ""), count: String(count ?? 3), ...(orientation ? { orientation } : {}) });
567
+ return asJson(await fetchImpl(`${baseUrl(cfg)}/media-search/${encodeURIComponent(slug)}?${q}`, { headers: authHeaders(token, cfg) }));
568
+ }
569
+ /** POST /api/media-import/<site> – Unsplash-Treffer übernehmen (Download-Trigger, Varianten, Attribution) → mediaId. */
570
+ export async function apiMediaImport(cfg, { site, resultId } = {}, fetchImpl = fetch) {
571
+ const { token, slug } = resolveTarget(cfg, site);
572
+ return asJson(await fetchImpl(`${baseUrl(cfg)}/media-import/${encodeURIComponent(slug)}`, {
573
+ method: "POST", headers: { ...authHeaders(token, cfg), "content-type": "application/json" }, body: JSON.stringify({ resultId }) }));
574
+ }
554
575
  /** GET /api/preview-screenshot/<site>?page=&width= – PNG des Entwurfs (Base64 + Maße). */
555
576
  export async function apiPreviewScreenshot(cfg, { site, page = "index.html", width = 1280 } = {}, fetchImpl = fetch) {
556
577
  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.13.7" }, { instructions: instructionsFor("local") });
24
+ const server = new McpServer({ name: "prontosite", version: "0.14.1" }, { 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.7",
3
+ "version": "0.14.1",
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, apiPageTexts, apiPageTextsSet, apiPageCopy, apiMediaUploadBegin, apiMediaUploadFile, apiMediaCommit, apiMediaList, apiImageSet, apiUndo, apiMediaDelete, apiMediaRestore, apiFormsGet, apiFormsSet, 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, apiEditHtml, apiPageTexts, apiPageTextsSet, apiPageCopy, apiMediaUploadBegin, apiMediaUploadFile, apiMediaCommit, apiMediaList, apiImageSet, apiUndo, apiMediaDelete, apiMediaRestore, apiFormsGet, apiFormsSet, apiMediaSearch, apiMediaImport, 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.).
@@ -29,6 +29,9 @@ const instructionLines = mode => [
29
29
  "2a. Für Text-, Link- und Kontaktdaten-Änderungen (Telefonnummer, E-Mail, Adresse, Öffnungszeiten) IMMER site_edit_text mit "
30
30
  + "scope:\"all\" (bei Bedarf region:\"footer\"/\"header\") – EIN Aufruf für alle Seiten, inkl. tel:/mailto:-Links. NIE Seite für Seite per "
31
31
  + "site_file_put. Meldet die Antwort company, zusätzlich site_company_set für die Firmendaten (Impressum/Datenschutz/Kontakt).",
32
+ "2c. Für Markup über alle Seiten (z. B. Footer-HTML) site_edit_html mit scope:\"all\" (ggf. region:\"footer\"), nie Seite für Seite per site_file_put. "
33
+ + "Schreib-Limit je 10 Minuten: Kunden 30, Betreuer 200 Aufrufe – dieselbe Änderung auf vielen Seiten ist EIN Aufruf (site_edit_text scope:\"all\" bzw. site_edit_html).",
34
+ "2d. Badge nie im HTML ändern, nachbauen oder entfernen – es wird beim Ausliefern erzeugt (data-ps=\"badge\"; site_edit_text/site_edit_html/site_file_put lehnen das ab, erlaubt ist nur das Wiederherstellen des Original-Markers).",
32
35
  "2b. Für Übersetzungen und Textüberarbeitung: site_page_texts → Texte übersetzen/überarbeiten → site_page_texts_set, JE SEITE ein Aufruf "
33
36
  + "(neue Sprachfassung vorher mit site_page_copy, z. B. index.html → en/index.html). NIE HTML über site_file_put umschreiben. "
34
37
  + "Rechtstext-Seiten (Impressum/Datenschutz) sind ausgenommen.",
@@ -40,6 +43,9 @@ const instructionLines = mode => [
40
43
  + "media_upload_commit und mit site_image_set einsetzen. Nie Varianten selbst bauen, nie ZIP für Medien.",
41
44
  ...(mode === "local" ? ["5a-lokal. Wenn du einen lokalen Dateipfad hast, nutze media_upload_file; sonst Upload-Link (media_upload_begin). "
42
45
  + "media_upload_file prüft, lädt hoch und übernimmt in einem Schritt und liefert die mediaId."] : []),
46
+ "5e. Bei „passendes Bild für Abschnitt X\": Abschnittstext lesen (site_page_texts), Suchbegriff bilden, media_search, dem Nutzer 2–3 "
47
+ + "Vorschauen zeigen und wählen lassen, dann media_import + site_image_set + site_preview_screenshot. Fotos NIE per URL von Unsplash "
48
+ + "oder anderen Bilddiensten (images.unsplash.com, imgix …) ins HTML/CSS schreiben – sonst lädt jeder Besucher beim Dienst; immer media_import.",
43
49
  "5b. Design = CSS-Datei ändern (site_file_get/site_file_put), nicht HTML umschreiben. Nach Design- oder Bild-Änderungen "
44
50
  + "site_preview_screenshot (1280 bzw. 390) prüfen, BEVOR du dem Nutzer antwortest.",
45
51
  "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 "
@@ -54,10 +60,10 @@ export const SERVER_INSTRUCTIONS = instructionsFor("remote");
54
60
  // Tool-Annotationen (MCP ToolAnnotations): ChatGPT & Co. fragen sonst bei JEDEM Aufruf nach. readOnly = ändert nichts;
55
61
  // destructive = live/kundenwirksam, ersetzt Bestand oder nimmt zurück (Bestätigung sinnvoll). Entwurfs-Änderungen sind
56
62
  // rückgängig machbar und daher nicht destruktiv. openWorld = wirkt nach außen (öffentliche Website/DNS).
57
- const READ_ONLY = new Set(["sites_list", "site_status", "site_forms_get", "site_page_texts", "site_media_list", "site_preview_screenshot", "site_company_get",
63
+ const READ_ONLY = new Set(["sites_list", "site_status", "site_forms_get", "media_search", "site_page_texts", "site_media_list", "site_preview_screenshot", "site_company_get",
58
64
  "site_domain_status", "site_embeds", "site_widgets", "site_files", "site_file_get"]);
59
65
  const DESTRUCTIVE = new Set(["site_publish", "site_deploy", "deploy_commit", "site_file_delete", "site_media_delete", "site_undo", "site_redo", "site_domain_connect"]);
60
- const OPEN_WORLD = new Set(["site_publish", "site_deploy", "deploy_commit", "site_domain_connect"]);
66
+ const OPEN_WORLD = new Set(["site_publish", "site_deploy", "deploy_commit", "site_domain_connect", "media_search", "media_import"]);
61
67
  export function toolAnnotations(name, mode = "local") {
62
68
  // site_export: remote nur ein Download-Link (lesend), lokal schreibt es eine ZIP auf die Platte.
63
69
  const ro = READ_ONLY.has(name) || (name === "site_export" && mode === "remote");
@@ -165,6 +171,22 @@ export function registerTools(server0, { z, cfg, mode = "local", remote = null }
165
171
  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); } }
166
172
  );
167
173
 
174
+ server.tool(
175
+ "site_edit_html",
176
+ "SCHREIBEND (Entwurf, nicht live) – Markup-Fragment über ALLE Seiten in EINEM Aufruf ersetzen (z. B. Footer-HTML: neuer Link, "
177
+ + "Social-Icons, Zeile ergänzen). Sucht `find` wörtlich im Quelltext (wie site_file_get) innerhalb von region (footer | header | "
178
+ + "main | all) und ersetzt durch `replace` – EIN Rückgängig-Schritt für alle Seiten. scope:\"all\" = alle Fundstellen ersetzen; ohne "
179
+ + "scope ändert der Server bei mehr als einer Fundstelle NICHTS und liefert die Liste (oder pages:[…]). Für reinen Text nimm "
180
+ + "site_edit_text. Rechtstext-Bereich, Skripte (Regeln wie site_file_put) und das ProntoSite-Badge sind geschützt (skipped).",
181
+ { site: z.string().optional().describe(SITE_DESC),
182
+ find: z.string().describe("Exaktes Markup, wie es im Quelltext steht (aus site_file_get kopieren)."),
183
+ replace: z.string().describe("Neues Markup."),
184
+ region: z.enum(["all", "header", "main", "footer"]).optional().describe("Nur in diesem Bereich suchen (erstes <footer>/<header>/<main> je Seite)."),
185
+ scope: z.enum(["all"]).optional().describe("\"all\" = alle Fundstellen auf allen (bzw. den gewählten) Seiten ersetzen."),
186
+ pages: z.array(z.string()).optional().describe("Nur diese Seiten (ID, Datei oder Slug)."),
187
+ baseRev: z.number().int().optional().describe("Optional: draftRev aus site_status (Konfliktschutz).") },
188
+ async (a) => { try { return asText(await apiEditHtml(cfg, a)); } catch (e) { return asError(e); } }
189
+ );
168
190
  server.tool(
169
191
  "site_page_texts",
170
192
  "LESEND – STANDARDWEG für Übersetzung/Umformulierung: liefert ALLE Texte einer Seite ohne HTML: [{id, role, text, markdown?}] – "
@@ -232,6 +254,32 @@ export function registerTools(server0, { z, cfg, mode = "local", remote = null }
232
254
  { site: z.string().optional().describe(SITE_DESC), uploadId: z.string().describe("uploadId aus media_upload_begin.") },
233
255
  async ({ site, uploadId }) => { try { return asText(await apiMediaCommit(cfg, { site, uploadId })); } catch (e) { return asError(e); } }
234
256
  );
257
+ server.tool(
258
+ "media_search",
259
+ "Kostenlose Fotos von Unsplash suchen (verbraucht ein gemeinsames Stunden-Kontingent; gleiche Suchen kommen 1 h aus dem Cache). "
260
+ + "Liefert je Treffer eine kleine Vorschau als Bild, Fotograf, Lizenz-Hinweis und eine resultId (1 h gültig). Suchbegriff am besten "
261
+ + "englisch und konkret (z. B. 'bakery counter bread'). Dem Nutzer 2–3 Vorschauen zeigen und wählen lassen, dann media_import "
262
+ + "(nie die Unsplash-URL direkt einbinden).",
263
+ { site: z.string().optional().describe(SITE_DESC), query: z.string().describe("Suchbegriff (2–120 Zeichen)."),
264
+ orientation: z.enum(["landscape", "portrait", "squarish"]).optional().describe("Ausrichtung: landscape (Hero/Banner), portrait, squarish."),
265
+ count: z.number().int().min(1).max(6).default(3).describe("Anzahl Treffer (max. 6).") },
266
+ async ({ site, query, orientation, count }) => {
267
+ try {
268
+ const r = await apiMediaSearch(cfg, { site, query, orientation, count });
269
+ const images = (r.results || []).filter(x => x.preview?.data).map(x => ({ type: "image", data: x.preview.data, mimeType: x.preview.mimeType || "image/jpeg" }));
270
+ const text = { ...r, results: (r.results || []).map((x, i) => ({ nr: i + 1, ...x, preview: x.preview ? "(Bild unten)" : null })) };
271
+ return { content: [{ type: "text", text: JSON.stringify(text, null, 2) }, ...images] };
272
+ } catch (e) { return asError(e); }
273
+ }
274
+ );
275
+ server.tool(
276
+ "media_import",
277
+ "SCHREIBEND: einen Unsplash-Treffer aus media_search übernehmen – der Server lädt das Foto (Download-Trigger laut Unsplash-Richtlinie), "
278
+ + "erzeugt Varianten, speichert Fotograf/Quelle in den Medien-Metadaten und liefert mediaId + vorbelegten alt-Text. Zählt aufs "
279
+ + "Medien-Kontingent. Danach site_image_set {mediaId, alt} und site_preview_screenshot.",
280
+ { site: z.string().optional().describe(SITE_DESC), resultId: z.string().describe("resultId aus media_search (1 h gültig).") },
281
+ async ({ site, resultId }) => { try { return asText(await apiMediaImport(cfg, { site, resultId })); } catch (e) { return asError(e); } }
282
+ );
235
283
  server.tool(
236
284
  "site_media_list",
237
285
  "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).",