@prontosite/mcp 0.11.2 → 0.12.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,12 +3,39 @@
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.11.2 – 2026-10-19
6
+ ## 0.12.1 – 2026-10-19
7
+
8
+ - **`site_edit_text` in einem Aufruf für die ganze Website:** `scope:"all"` ersetzt alle Fundstellen auf allen Seiten sofort
9
+ (ein Rückgängig-Schritt), `region` (`footer`/`header`/`main`/`all`) begrenzt auf einen Bereich, `attributes` (Standard an)
10
+ ersetzt auch in Links (`tel:`/`mailto:` normalisiert, Ländervorwahl bleibt) und Attributen (`alt`, `title`, `meta content`).
11
+ Antwort je Seite gefunden/geändert; steht der Wert in den Firmendaten, verweist `company` auf `site_company_set`.
12
+ - Instructions: Kontaktdaten-, Text- und Link-Änderungen immer per `site_edit_text` mit `scope:"all"`, nie Seite für Seite per
13
+ `site_file_put`.
14
+
15
+ ## 0.12.0 – 2026-10-19
16
+
17
+ - **Arbeitsweise im Server verankert:** Der MCP-Server liefert beim Verbinden `instructions` – Standard ist das Arbeiten direkt
18
+ am Server-Entwurf (`site_status` → `site_edit_text`/`site_file_*` → `site_status`, Vorschau-Link nennen). Export + Deploy nur
19
+ auf ausdrücklichen Wunsch bzw. für größere Umbauten; `site_publish` nur nach Bestätigung im Chat; bei 409 erst `site_status`.
20
+ - **Neu `site_edit_text`** (`find`, `replace`, optional `pages`, `baseRev`): Text über alle Seiten suchen und ersetzen –
21
+ Formatierung bleibt, ein Rückgängig-Schritt, Rechtstexte ausgenommen; bei mehreren Fundstellen ohne `pages` nur eine Liste.
22
+ - Beschreibungen: Datei-Tools „STANDARDWEG", Export/Deploy „AUSNAHME"; `site_publish` ausdrücklich „live für Besucher, nur nach
23
+ Bestätigung".
24
+ - **`baseRev` beim Deploy-Update Pflicht** (F4a): ZIPs aus `site_export` tragen ihn automatisch, sonst `baseRev` aus `site_status`
25
+ mitgeben (neuer Parameter an `site_deploy`); ohne → `428`.
26
+
27
+ ## 0.11.3 – 2026-10-19
28
+
29
+ - Beschreibungen von `site_file_put`/`site_file_delete`/`site_file_rename`: Schreiben geht jetzt auch mit Kunden-Token
30
+ (serverseitig seit 19.10.); Skriptdateien (JavaScript) bleiben Betreuern vorbehalten – Kunden bekommen dort 403.
31
+
32
+ ## 0.11.2 – 2026-10-19 (veröffentlicht 19.10.2026)
7
33
 
8
34
  - **Intern, kein Verhaltenswechsel:** die Tool-Definitionen liegen jetzt in `tools.mjs` – dieselbe Quelle nutzt der neue
9
35
  Remote-Endpunkt `https://app.prontosite.io/mcp` (Streamable HTTP, Bearer-Token; für Cursor und Claude Code ohne lokale
10
36
  Installation). Lokal bleiben `site_export`/`site_deploy` mit Dateipfaden; remote heißen sie Download-Link bzw.
11
37
  `deploy_upload_begin` + `deploy_commit`.
38
+ - Die Config-Prüfung akzeptiert neben `psk_` auch `pso_` (OAuth-Zugriff – nur der Remote-Endpunkt reicht ihn intern weiter; lokal ändert sich nichts).
12
39
  - MCP-SDK mindestens 1.31.0 (Sicherheitshinweis GHSA-6qxp-vccf-f47h betrifft nur die OAuth-Client-Seite, hier ungenutzt).
13
40
 
14
41
  ## 0.11.1 – 2026-10-18
package/api.mjs CHANGED
@@ -43,7 +43,8 @@ export function loadConfig(env = process.env, home = os.homedir()) {
43
43
  }
44
44
 
45
45
  const needToken = (token, ref) => {
46
- if (!token || !String(token).startsWith("psk_"))
46
+ // psk_ = API-Token; pso_ = OAuth-Zugriff (nur der Remote-Endpunkt /mcp reicht ihn an seinen internen Loopback weiter).
47
+ if (!token || !/^(psk_|pso_)/.test(String(token)))
47
48
  throw new Error(`Kein API-Token für „${ref}" konfiguriert. Setze PRONTOSITE_TOKEN oder trage die Site in ~/.prontosite.json (sites) ein.`);
48
49
  return token;
49
50
  };
@@ -155,7 +156,7 @@ export async function apiExport(cfg, { site, which = "published", outDir = "." }
155
156
  }
156
157
 
157
158
  /** POST /api/deploy/<site> – ZIP übergeben (+ optional publish/resetOverlay/legal). site="new" = anlegen. */
158
- export async function apiDeploy(cfg, { site, customer = null, slug: wishSlug = null, name = null, zipPath, publish = false, resetOverlay = false, legal = null, company = null, siteOverride = false } = {}, fetchImpl = fetch) {
159
+ export async function apiDeploy(cfg, { site, customer = null, slug: wishSlug = null, name = null, zipPath, baseRev = null, publish = false, resetOverlay = false, legal = null, company = null, siteOverride = false } = {}, fetchImpl = fetch) {
159
160
  if (!zipPath || !fs.existsSync(zipPath)) throw new Error(`ZIP nicht gefunden: ${zipPath}`);
160
161
  const target = resolveTarget(cfg, site);
161
162
  const { token, slug } = target;
@@ -172,7 +173,8 @@ export async function apiDeploy(cfg, { site, customer = null, slug: wishSlug = n
172
173
  if (legal === false) q.set("legal", "0");
173
174
  if (effCustomer) q.set("customer", effCustomer); // Betreuer-Token + „new": Ziel-Konto für den Erst-Import
174
175
  if (wishSlug) q.set("slug", String(wishSlug)); // Erst-Import: Wunsch-Adresse (sonst wird sie abgeleitet)
175
- if (isNew && name) q.set("name", String(name)); // Erst-Import: Site-Name explizit setzen (sonst aus der Startseite abgeleitet)
176
+ if (isNew && name) q.set("name", String(name));
177
+ if (!isNew && Number.isInteger(baseRev)) q.set("baseRev", String(baseRev)); // F4a: ab 0.12 Pflicht bei Updates (ZIP aus site_export trägt ihn selbst) // Erst-Import: Site-Name explizit setzen (sonst aus der Startseite abgeleitet)
176
178
  const qs = q.toString() ? `?${q}` : "";
177
179
  const res = await fetchImpl(`${baseUrl(cfg)}/deploy/${encodeURIComponent(slug)}${qs}`, {
178
180
  method: "POST", headers: { ...authHeaders(token, cfg), "content-type": "application/zip" }, body: fs.readFileSync(zipPath) });
@@ -386,6 +388,15 @@ export async function apiFileRename(cfg, { site, from, to, baseRev } = {}, fetch
386
388
  method: "POST", headers: { ...authHeaders(token, cfg), "content-type": "application/json" }, body: JSON.stringify({ from, to, baseRev }) }));
387
389
  }
388
390
 
391
+ /** POST /api/edit-text/<site> – Text über alle Seiten suchen + ersetzen (Entwurf, eine Undo-Einheit). 409 kommt als Objekt zurück. */
392
+ export async function apiEditText(cfg, { site, find, replace, scope = null, region = null, attributes = null, pages = null, baseRev = null } = {}, fetchImpl = fetch) {
393
+ const { token, slug } = resolveTarget(cfg, site);
394
+ const body = { find, replace, ...(scope ? { scope } : {}), ...(region ? { region } : {}), ...(attributes === false ? { attributes: false } : {}),
395
+ ...(Array.isArray(pages) && pages.length ? { pages } : {}), ...(Number.isInteger(baseRev) ? { baseRev } : {}) };
396
+ return asJsonOr409(await fetchImpl(`${baseUrl(cfg)}/edit-text/${encodeURIComponent(slug)}`, {
397
+ method: "POST", headers: { ...authHeaders(token, cfg), "content-type": "application/json" }, body: JSON.stringify(body) }));
398
+ }
399
+
389
400
  /** GET/POST /api/lock/<site> – sanfte Sperre („in Bearbeitung durch …"); blockiert niemanden. action: lock|unlock|takeover. */
390
401
  export async function apiLock(cfg, { site, action = null, note = null } = {}, fetchImpl = fetch) {
391
402
  const { token, slug } = resolveTarget(cfg, site);
package/index.mjs CHANGED
@@ -14,13 +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 } from "./tools.mjs";
17
+ import { registerTools, SERVER_INSTRUCTIONS } 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
- const server = new McpServer({ name: "prontosite", version: "0.11.2" });
23
+ // instructions: Arbeitsweise (Weg B = Standard) – gleicher Text wie am Remote-Endpunkt.
24
+ const server = new McpServer({ name: "prontosite", version: "0.12.1" }, { instructions: SERVER_INSTRUCTIONS });
24
25
  // Tool-Definitionen: gemeinsame Quelle mit dem Remote-Endpunkt (tools.mjs, Konzept KI-Anbindung §4.2).
25
26
  registerTools(server, { z, cfg, mode: "local" });
26
27
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@prontosite/mcp",
3
- "version": "0.11.2",
3
+ "version": "0.12.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,27 @@
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 { 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, 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
+
14
+ /**
15
+ * Arbeitsweise (MCP-`instructions` beim Initialize – lokal UND /mcp): Weg B ist der Standard (Nick 19.10.).
16
+ * Direkt am Server-Entwurf arbeiten; Export + Deploy nur auf ausdrücklichen Wunsch bzw. für Komplett-Umbauten.
17
+ */
18
+ export const SERVER_INSTRUCTIONS = [
19
+ "ProntoSite – so arbeitest du mit einer Website:",
20
+ "1. STANDARD: direkt am Server-Entwurf arbeiten. Ablauf: site_status (Stand, draftRev, wer zuletzt geändert hat) → Änderung mit "
21
+ + "site_edit_text (Text suchen und ersetzen über alle Seiten) bzw. site_file_get/site_file_put (HTML/CSS) → site_status (Kontrolle). "
22
+ + "Danach dem Nutzer sagen, was geändert wurde, und den Vorschau-Link (previewUrl aus site_status) nennen.",
23
+ "2. AUSNAHME: site_export + Deploy (site_deploy bzw. deploy_upload_begin/deploy_commit) NUR, wenn der Nutzer ausdrücklich lokal arbeiten "
24
+ + "möchte oder die Website komplett umgebaut wird.",
25
+ "2a. Für Text-, Link- und Kontaktdaten-Änderungen (Telefonnummer, E-Mail, Adresse, Öffnungszeiten) IMMER site_edit_text mit "
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
+ + "site_file_put. Meldet die Antwort company, zusätzlich site_company_set für die Firmendaten (Impressum/Datenschutz/Kontakt).",
28
+ "3. Veröffentlichen (site_publish, publish=true) NUR nach ausdrücklicher Bestätigung des Nutzers im Chat – bis dahin bleibt alles im Entwurf.",
29
+ "4. Konflikt (409, conflict:true): nicht erzwingen. Erst site_status aufrufen (zeigt, wer was geändert hat), Inhalt neu holen, Änderung "
30
+ + "neu anwenden und mit der neuen draftRev erneut senden.",
31
+ "5. Jede Änderung lässt sich rückgängig machen; Rechtstexte (Impressum/Datenschutz) nicht umformulieren – sie kommen aus den Firmendaten.",
32
+ ].join("\n");
13
33
 
14
34
  const asText = obj => ({ content: [{ type: "text", text: JSON.stringify(obj, null, 2) }] });
15
35
  const asError = e => ({ isError: true, content: [{ type: "text", text: "Fehler: " + (e?.message || String(e)) }] });
@@ -80,18 +100,40 @@ export function registerTools(server, { z, cfg, mode = "local", remote = null })
80
100
  }
81
101
  server.tool(
82
102
  "site_status",
83
- "Status einer ProntoSite-Website: Plan, Slug, Live-URL, letzte Änderungen, offener Veröffentlichungs"
84
- + "bedarf, draftRev und recentEdits (wer hat zuletzt wo geändert). VOR Änderungen lesen – draftRev ist die baseRev für "
85
- + "site_file_put. LESEND – ändert nichts.",
103
+ "LESEND – SCHRITT 1 und letzter Schritt jeder Änderung: Status einer ProntoSite-Website: Plan, Slug, Live-URL, Vorschau-Link "
104
+ + "(previewUrl), letzte Änderungen, offener Veröffentlichungsbedarf (publishNeeded), draftRev und recentEdits (wer hat zuletzt "
105
+ + "wo geändert). draftRev ist die baseRev für site_edit_text/site_file_put. Ändert nichts.",
86
106
  { site: z.string().optional().describe(SITE_DESC) },
87
107
  async ({ site }) => { try { return asText(await apiStatus(cfg, { site })); } catch (e) { return asError(e); } }
88
108
  );
89
109
 
110
+ server.tool(
111
+ "site_edit_text",
112
+ "SCHREIBEND (Entwurf, nicht live) – STANDARDWEG für Textänderungen (Telefonnummer, E-Mail, Öffnungszeiten, Preise, Sätze): sucht "
113
+ + "`find` im sichtbaren Text UND (attributes, Standard an) in Links/Attributen (href inkl. tel:/mailto: – normalisiert –, alt, title, "
114
+ + "meta content) ALLER Seiten und ersetzt durch `replace` – EIN Aufruf, EIN Rückgängig-Schritt, Formatierung bleibt. "
115
+ + "scope:\"all\" = alle Fundstellen auf allen Seiten sofort ersetzen (für Kontaktdaten im Kopf-/Fußbereich: plus region:\"footer\" bzw. "
116
+ + "\"header\"); ohne scope ändert der Server bei mehr als einer Fundstelle NICHTS und liefert nur die Liste (oder pages:[…]). "
117
+ + "Antwort: pages = je Seite gefunden/geändert. Rechtstexte und geschützte Bereiche bleiben unberührt (skipped); steht der Wert in "
118
+ + "den Firmendaten, nennt `company` das Feld → zusätzlich site_company_set. Nie stattdessen Seite für Seite site_file_put. "
119
+ + "Vorher site_status, danach site_status; live erst nach Bestätigung mit site_publish.",
120
+ { site: z.string().optional().describe(SITE_DESC),
121
+ find: z.string().describe("Genau der Text, wie er auf der Website steht (z. B. '02242 123456')."),
122
+ replace: z.string().describe("Neuer Text (reiner Text)."),
123
+ scope: z.enum(["all"]).optional().describe("\"all\" = alle Fundstellen auf allen (bzw. den per pages gewählten) Seiten in diesem Aufruf ersetzen. Standard für Kontaktdaten."),
124
+ region: z.enum(["all", "header", "main", "footer"]).default("all").describe("Nur in diesem Bereich suchen: footer (Fußbereich), header (Kopf/Navigation), main (Inhalt) oder all."),
125
+ attributes: z.boolean().default(true).describe("Auch Linkziele (href, tel:/mailto: normalisiert) und Attribute (alt, title, meta content) ersetzen."),
126
+ pages: z.array(z.string()).optional().describe("Optional: Seiten-IDs (aus der Fundstellen-Liste), auf denen ersetzt werden soll. Ohne pages und ohne scope:\"all\" nur bei genau EINER Fundstelle."),
127
+ baseRev: z.number().int().optional().describe("Optional: draftRev aus site_status (Konfliktschutz, 409 bei zwischenzeitlicher Änderung derselben Seite).") },
128
+ 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
+ );
130
+
90
131
  if (mode === "local") {
91
132
  server.tool(
92
133
  "site_export",
93
- "Exportiert eine Website als ZIP in ein lokales Verzeichnis. which: 'published' (Live-Stand, Standard), "
94
- + "'draft' (Entwurf) oder 'original' (flacher Stand ohne ProntoSite-Zutaten). LESEND.",
134
+ "AUSNAHME – nur wenn der Nutzer ausdrücklich lokal arbeiten möchte oder für größere Umbauten (sonst site_edit_text/site_file_*): "
135
+ + "Exportiert eine Website als ZIP in ein lokales Verzeichnis. which: 'published' (Live-Stand, Standard), "
136
+ + "'draft' (Entwurf – zum Weiterbearbeiten) oder 'original' (flacher Stand ohne ProntoSite-Zutaten). LESEND.",
95
137
  { site: z.string().optional().describe(SITE_DESC),
96
138
  which: z.enum(["published", "draft", "original"]).default("published"),
97
139
  outDir: z.string().default(".").describe("Zielordner für die ZIP-Datei (lokal).") },
@@ -101,7 +143,9 @@ export function registerTools(server, { z, cfg, mode = "local", remote = null })
101
143
 
102
144
  server.tool(
103
145
  "site_deploy",
104
- "SCHREIBEND: Übergibt eine lokale ZIP-Datei an ProntoSite (Import). Mit publish=true wird die Website "
146
+ "SCHREIBEND – AUSNAHME: nur für Erst-Import, ausdrücklichen Wunsch nach lokaler Arbeit oder größere Umbauten (kleine Änderungen: "
147
+ + "site_edit_text/site_file_put). Übergibt eine lokale ZIP-Datei an ProntoSite (Import). Bei Updates PFLICHT: baseRev – exportiere "
148
+ + "vorher mit site_export (which='draft'), die ZIP trägt ihren Stand dann automatisch; sonst baseRev = draftRev aus site_status. Mit publish=true wird die Website "
105
149
  + "DIREKT LIVE geschaltet und der bestehende Live-Stand überschrieben. ERST-IMPORT (neue Website): "
106
150
  + "Einzel-Config site='new'; Multi-Site/Betreuer site='<label>:new' (legt im Konto DES LABELS an). "
107
151
  + "WICHTIG beim ERST-IMPORT: Frag den Nutzer VOR dem Deploy nach der gewünschten Adresse "
@@ -130,13 +174,14 @@ export function registerTools(server, { z, cfg, mode = "local", remote = null })
130
174
  name: z.string().optional().describe("Erst-Import: expliziter Website-Name (überschreibt die Ableitung aus der Startseite). Nützlich, wenn der Titel einen Ortszusatz trägt (z. B. 'Physio Phoenix – Bad Honnef Süd' → name='Physio Phoenix'). Bei Updates ignoriert."),
131
175
  customer: z.string().optional().describe("Betreuer-Token + Erst-Import: Ziel-Konto-ID (k-…) für die neue Website. Alternativ direkt im site-Argument als '<Konto-ID>:new'."),
132
176
  zipPath: z.string().describe("Pfad zur ZIP-Datei der Website (lokal)."),
177
+ baseRev: z.number().int().optional().describe("Update ohne Export-ZIP: draftRev aus site_status (sonst lehnt der Server mit 428 ab). Bei ZIPs aus site_export nicht nötig."),
133
178
  publish: z.boolean().default(false).describe("true = nach dem Import direkt veröffentlichen (LIVE)."),
134
179
  resetOverlay: z.boolean().default(false).describe("true = bisherige Dashboard-Änderungen verwerfen."),
135
180
  legal: z.boolean().optional().describe("Rechts-Automatik (Impressum/Datenschutz): true erzwingt, false schaltet ab."),
136
181
  company: companyObject.optional().describe("Optional: Firmendaten VOR dem Deploy setzen (ein Schritt) – Impressum/Datenschutz nutzen sie dann direkt. Teil-Update."),
137
182
  siteOverride: z.boolean().default(false).describe("Nur mit company: true = Firmendaten NUR für diese Website (statt konto-weit).") },
138
- async ({ site, slug, name, customer, zipPath, publish, resetOverlay, legal, company, siteOverride }) => {
139
- try { return asText(await apiDeploy(cfg, { site, slug, name, customer, zipPath, publish, resetOverlay, legal, company, siteOverride })); } catch (e) { return asError(e); }
183
+ async ({ site, slug, name, customer, zipPath, baseRev, publish, resetOverlay, legal, company, siteOverride }) => {
184
+ try { return asText(await apiDeploy(cfg, { site, slug, name, customer, zipPath, baseRev, publish, resetOverlay, legal, company, siteOverride })); } catch (e) { return asError(e); }
140
185
  }
141
186
  );
142
187
 
@@ -145,9 +190,9 @@ export function registerTools(server, { z, cfg, mode = "local", remote = null })
145
190
  }
146
191
  server.tool(
147
192
  "site_publish",
148
- "SCHREIBEND: Veröffentlicht den aktuellen Stand einer Website ERNEUT (ohne ZIP) – z. B. um Plattform-"
149
- + "Aktualisierungen live zu bringen. Meldet die Antwort nothing:true (keine erkannte Änderung), aber du "
150
- + "willst trotzdem neu ausliefern, setze force:true. Bitte VOR dem Aufruf bestätigen lassen – und die richtige Website prüfen.",
193
+ "SCHREIBEND – LIVE FÜR BESUCHER: Veröffentlicht den aktuellen Entwurf einer Website. NUR nach ausdrücklicher Bestätigung "
194
+ + "des Nutzers im Chat aufrufen (vorher kurz zusammenfassen, was live geht, und die richtige Website prüfen). Meldet die Antwort "
195
+ + "nothing:true (keine erkannte Änderung), aber du willst trotzdem neu ausliefern, setze force:true.",
151
196
  { site: z.string().optional().describe(SITE_DESC),
152
197
  force: z.boolean().default(false).describe("true = trotzdem veröffentlichen, auch ohne erkannte Änderung (wie der Menüpunkt Erneut veröffentlichen).") },
153
198
  async ({ site, force }) => { try { return asText(await apiPublish(cfg, { site, force })); } catch (e) { return asError(e); } }
@@ -290,7 +335,9 @@ export function registerTools(server, { z, cfg, mode = "local", remote = null })
290
335
  );
291
336
 
292
337
  // ---- Datei-API: kleine Änderungen am Entwurf direkt auf dem Server (Konzept §7) ----
293
- const FILE_SMALL = "Für kleine Änderungen am Entwurf ohne lokale Kopie; für größere Umbauten site_export (which='draft') → lokal bearbeiten → site_deploy. ";
338
+ const FILE_SMALL = "STANDARDWEG: Änderungen direkt am Server-Entwurf (für reine Textänderungen am einfachsten site_edit_text); "
339
+ + "Export + Deploy nur als Ausnahme für größere Umbauten oder auf ausdrücklichen Wunsch. "
340
+ + "Skriptdateien (JavaScript) ändern nur Betreuer und nur bei JavaScript-Freigabe der Website – Kunden bekommen dort 403. ";
294
341
  server.tool(
295
342
  "site_files",
296
343
  "LESEND: Listet die Dateien des Entwurfs einer Website (Pfad, Größe, Typ, ob bearbeitbar) und die aktuelle draftRev. "
@@ -308,7 +355,7 @@ export function registerTools(server, { z, cfg, mode = "local", remote = null })
308
355
  );
309
356
  server.tool(
310
357
  "site_file_put",
311
- "SCHREIBEND (Entwurf, nicht live; nur Betreuer-Token): Ersetzt den Inhalt einer Text-Datei (HTML einer bestehenden Seite, "
358
+ "SCHREIBEND (Entwurf, nicht live): Ersetzt den Inhalt einer Text-Datei (HTML einer bestehenden Seite, "
312
359
  + "CSS, SVG, JSON, TXT …). PFLICHT: baseRev = draftRev aus deinem letzten site_file_get/site_status. Antwortet der Server mit "
313
360
  + "conflict:true (409), hat jemand anderes dieselbe Datei inzwischen geändert: NICHT erzwingen – Datei neu holen, Änderung neu "
314
361
  + "anwenden, mit der neuen draftRev erneut senden; die Antwort listet wer/wann/was. Prüfe danach overlay (umgehängte/deaktivierte "
@@ -324,7 +371,7 @@ export function registerTools(server, { z, cfg, mode = "local", remote = null })
324
371
  );
325
372
  server.tool(
326
373
  "site_file_delete",
327
- "SCHREIBEND (Entwurf; nur Betreuer-Token): Markiert eine Text-Datei im Entwurf als gelöscht (rückgängig machbar; Seiten "
374
+ "SCHREIBEND (Entwurf): Markiert eine Text-Datei im Entwurf als gelöscht (rückgängig machbar; Seiten "
328
375
  + "löschen geht über das Dashboard). baseRev Pflicht, confirmDestructive:true erst NACH Rückfrage beim Nutzer. Prüfe vorher, "
329
376
  + "ob die Datei noch verlinkt ist. " + FILE_SMALL,
330
377
  { site: z.string().optional().describe(SITE_DESC), path: z.string(), baseRev: z.number().int(),
@@ -333,7 +380,7 @@ export function registerTools(server, { z, cfg, mode = "local", remote = null })
333
380
  );
334
381
  server.tool(
335
382
  "site_file_rename",
336
- "SCHREIBEND (Entwurf; nur Betreuer-Token): Benennt eine Text-Datei (kein HTML) um. Verweise in anderen Dateien werden NICHT "
383
+ "SCHREIBEND (Entwurf): Benennt eine Text-Datei (kein HTML) um. Verweise in anderen Dateien werden NICHT "
337
384
  + "angepasst – die musst du selbst per site_file_put nachziehen. baseRev Pflicht. " + FILE_SMALL,
338
385
  { site: z.string().optional().describe(SITE_DESC), from: z.string(), to: z.string(), baseRev: z.number().int() },
339
386
  async ({ site, from, to, baseRev }) => { try { return asText(await apiFileRename(cfg, { site, from, to, baseRev })); } catch (e) { return asError(e); } }
@@ -362,7 +409,8 @@ export function registerTools(server, { z, cfg, mode = "local", remote = null })
362
409
  function registerRemoteTransferTools(server, { z, cfg, remote, SITE_DESC, asText, asError }) {
363
410
  server.tool(
364
411
  "site_export",
365
- "LESEND: Exportiert eine Website als ZIP und liefert einen DOWNLOAD-LINK (15 Minuten gültig, nur EINMAL abrufbar). "
412
+ "AUSNAHME – nur wenn der Nutzer ausdrücklich lokal arbeiten möchte oder für größere Umbauten (sonst site_edit_text/site_file_*). "
413
+ + "LESEND: Exportiert eine Website als ZIP und liefert einen DOWNLOAD-LINK (15 Minuten gültig, nur EINMAL abrufbar). "
366
414
  + "which: 'published' (Live-Stand, Standard), 'draft' (Entwurf – für lokales Bearbeiten und späteres deploy_upload_begin) oder "
367
415
  + "'original' (flacher Stand ohne ProntoSite-Zutaten). Den Link mit curl/Browser laden, z. B. curl -o site.zip '<downloadUrl>'.",
368
416
  { site: z.string().optional().describe(SITE_DESC),
@@ -376,7 +424,8 @@ function registerRemoteTransferTools(server, { z, cfg, remote, SITE_DESC, asText
376
424
  );
377
425
  server.tool(
378
426
  "deploy_upload_begin",
379
- "SCHREIBEND (Schritt 1 von 2): ZIP-Deploy vorbereiten. Liefert einen UPLOAD-LINK (HTTP PUT, 15 Minuten, einmalig, max. "
427
+ "SCHREIBEND – AUSNAHME: nur für Erst-Import, ausdrücklichen Wunsch nach lokaler Arbeit oder größere Umbauten (kleine Änderungen: "
428
+ + "site_edit_text/site_file_put). Schritt 1 von 2: ZIP-Deploy vorbereiten. Liefert einen UPLOAD-LINK (HTTP PUT, 15 Minuten, einmalig, max. "
380
429
  + "100 MB) für die ZIP-Datei der Website, z. B. curl -T site.zip '<uploadUrl>'. Danach deploy_commit mit der uploadId. "
381
430
  + "Ein Deploy ersetzt das Original der Website (rückholbar über die Re-Import-Zeitleiste); mit publish=true geht der Stand "
382
431
  + "DIREKT LIVE. Ab dem Starter-Tarif. Bitte VOR dem Aufruf bestätigen lassen – und die richtige Website prüfen. "
@@ -387,9 +436,10 @@ function registerRemoteTransferTools(server, { z, cfg, remote, SITE_DESC, asText
387
436
  name: z.string().optional().describe("Erst-Import: expliziter Website-Name. Bei Updates ignoriert."),
388
437
  publish: z.boolean().default(false).describe("true = nach dem Import direkt veröffentlichen (LIVE)."),
389
438
  resetOverlay: z.boolean().default(false).describe("true = bisherige Dashboard-Änderungen verwerfen."),
390
- legal: z.boolean().optional().describe("Rechts-Automatik (Impressum/Datenschutz): true erzwingt, false schaltet ab.") },
391
- async ({ site, slug, name, publish, resetOverlay, legal }) => {
392
- try { return asText(remote.beginUpload({ site, slug, name, publish, resetOverlay, legal })); } catch (e) { return asError(e); }
439
+ legal: z.boolean().optional().describe("Rechts-Automatik (Impressum/Datenschutz): true erzwingt, false schaltet ab."),
440
+ baseRev: z.number().int().optional().describe("Update ohne Export-ZIP: draftRev aus site_status (Pflicht, sonst 428). ZIPs aus site_export tragen ihn automatisch.") },
441
+ async ({ site, slug, name, publish, resetOverlay, legal, baseRev }) => {
442
+ try { return asText(remote.beginUpload({ site, slug, name, publish, resetOverlay, legal, baseRev })); } catch (e) { return asError(e); }
393
443
  }
394
444
  );
395
445
  server.tool(
@@ -404,7 +454,7 @@ function registerRemoteTransferTools(server, { z, cfg, remote, SITE_DESC, asText
404
454
  up = remote.takeUpload(uploadId);
405
455
  const o = up.options;
406
456
  return asText(await apiDeploy(cfg, { site: o.site, slug: o.slug, name: o.name, zipPath: up.path, publish: o.publish,
407
- resetOverlay: o.resetOverlay, legal: o.legal }));
457
+ resetOverlay: o.resetOverlay, legal: o.legal, baseRev: o.baseRev }));
408
458
  } catch (e) { return asError(e); } finally { if (up) up.done(); }
409
459
  }
410
460
  );