@prontosite/mcp 0.10.3 → 0.11.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,7 +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.10.3 – 2026-10-14
6
+ ## 0.11.1 – 2026-10-18
7
+
8
+ - **Sanfte Sperre:** neue Tools `site_lock` (Notiz optional, `takeover` nur Betreuer) und `site_unlock`. Zeigt allen anderen
9
+ „In Bearbeitung durch … seit …“ (Dashboard + `site_status.lock`), blockiert niemanden; wer trotzdem speichert, bekommt
10
+ `lockWarning`. Läuft 2 Stunden nach der letzten Änderung ab.
11
+ - **Seiten per Datei-Tools** (Server-seitig): `site_file_put` mit einer NEUEN `.html`-Datei legt eine Seite an;
12
+ `site_file_delete` / `site_file_rename` können Seiten löschen/umbenennen (Startseite geschützt, Verweise auf die alte
13
+ Adresse werden gemeldet, nicht umgeschrieben).
14
+
15
+ ## 0.11.0 – 2026-10-18 (veröffentlicht 18.10.2026)
16
+
17
+ - **Datei-API – kleine Änderungen am Entwurf ohne lokale Kopie:** neue Tools `site_files`, `site_file_get`,
18
+ `site_file_put`, `site_file_delete`, `site_file_rename` (Schreiben nur mit Betreuer-Token). Pflicht-`baseRev`
19
+ (= `draftRev` aus `site_status`/`site_file_get`); bei Konflikt (jemand hat dieselbe Datei geändert) kommt die
20
+ Antwort mit `conflict:true` und der Liste wer/wann/was zurück – nicht als Fehler, damit der Assistent neu aufsetzt.
21
+ Für größere Umbauten bleibt `site_export (which='draft')` → lokal → `site_deploy`.
22
+ - **`site_status`** beschreibt `draftRev` und `recentEdits` (vor Änderungen lesen).
23
+ - **Client-Kennung:** jeder Aufruf sendet `X-ProntoSite-Client: mcp/<version>` – im Verlauf je Site erscheint der Weg
24
+ „MCP" statt „API".
25
+ - Server-seitig (wirkt auch für ältere Versionen): Export-ZIP trägt `.prontosite-export.json` mit `draftRev`;
26
+ `site_deploy` eines exportierten Stands meldet einen zwischenzeitlich geänderten Entwurf mit 409 statt zu überschreiben.
27
+
28
+ ## 0.10.3 – 2026-10-14 (veröffentlicht 18.10.2026)
7
29
 
8
30
  - **`site_deploy`:** neuer Parameter `name` (Erst-Import) – setzt den Website-Namen explizit, falls die
9
31
  Ableitung aus der Startseite danebenliegt (z. B. „Physio Phoenix – Bad Honnef Süd" → `name="Physio Phoenix"`).
package/README.md CHANGED
@@ -83,7 +83,12 @@ sichern: `cp ~/.prontosite.json ~/.prontosite.json.bak`).
83
83
  |------|---------|
84
84
  | `sites_list` | Alle erreichbaren Websites (Slug, Plan, letzte Änderung, Publish-Bedarf). **Lesend.** |
85
85
  | `account_create({ name, email?, plan?, note?, override? })` | **Nur mit Betreuer-Token.** Legt ein neues Kundenkonto an (Dashboard-Weg „Kunde anlegen"). Mit `email` → eingeladenes Login-Konto (Invite-Mail), ohne → verwaltetes Konto. Partner/Agentur: Konto wird dem Betreuer zugeordnet (Agentur-Kunden automatisch `agentur-kunde`). Antwort nennt die neue Konto-ID (k-…) → danach `site_deploy({ site: "<k-id>:new", zipPath })`. Namens-/E-Mail-Dublette → Warnung mit vorhandener k-ID (`override:true` erzwingt die Namensdublette). **Schreibend.** |
86
- | `site_status({ site? })` | Plan, Slug, Live-URL, Publish-Bedarf. **Lesend.** |
86
+ | `site_status({ site? })` | Plan, Slug, Live-URL, Publish-Bedarf, `draftRev` + `recentEdits` (wer hat zuletzt wo geändert). **Lesend.** |
87
+ | `site_files({ site? })` | Dateien des Entwurfs (Pfad, Größe, Typ, bearbeitbar) + `draftRev`. **Lesend.** |
88
+ | `site_file_get({ site?, path })` | Quelltext einer Datei (Text bzw. Base64, max. 5 MB) + `draftRev` + Hinweis auf Dashboard-Änderungen darüber (`overlay`). **Lesend.** |
89
+ | `site_file_put({ site?, path, content, baseRev, overlay? })` | Text-Datei im Entwurf ersetzen (nur Betreuer-Token). `baseRev` Pflicht; bei Konflikt `conflict:true` mit Liste statt Überschreiben. Nicht live – danach `site_publish`. **Schreibend.** |
90
+ | `site_file_delete({ site?, path, baseRev, confirmDestructive? })` / `site_file_rename({ site?, from, to, baseRev })` | Text-Datei im Entwurf entfernen bzw. umbenennen; eine Seite löschen/umbenennen (Startseite geschützt, Verweise werden gemeldet). Eine neue `.html` per `site_file_put` legt eine neue Seite an. Undo-fähig, nur Betreuer-Token. **Schreibend.** |
91
+ | `site_lock({ site?, note?, takeover? })` / `site_unlock({ site? })` | Sanfte Sperre „In Bearbeitung durch …“ (2 h, verlängert sich bei jeder Änderung). Blockiert niemanden; `takeover` nur als Betreuer. |
87
92
  | `site_export({ site?, which, outDir })` | ZIP (`published`\|`draft`\|`original`) lokal speichern. **Lesend.** |
88
93
  | `site_deploy({ site?, zipPath, publish?, resetOverlay?, legal?, company?, siteOverride? })` | ZIP importieren, optional live; optional Firmendaten im selben Aufruf setzen. Beim lokalen Bauen Bausteine im HTML per `data-ps` markieren (`formular:kontakt`, `impressum`/`datenschutz`, `link-impressum`/`link-datenschutz`) → [Marker-Syntax](https://prontosite.io/docs/marker-shortcodes). **Schreibend.** |
89
94
  | `site_publish({ site? })` | Aktuellen Stand erneut veröffentlichen. **Schreibend.** |
package/api.mjs CHANGED
@@ -92,7 +92,10 @@ export function resolveTarget(cfg, site) {
92
92
  }
93
93
 
94
94
  const baseUrl = cfg => `https://${cfg.host}/api`;
95
- const authHeaders = token => ({ authorization: `Bearer ${token}` });
95
+ // Client-Kennung: der Server führt damit im Verlauf je Site den Weg „mcp" (statt „api" für curl).
96
+ let CLIENT_ID = "mcp";
97
+ try { CLIENT_ID = "mcp/" + JSON.parse(fs.readFileSync(new URL("./package.json", import.meta.url), "utf8")).version; } catch { /* egal */ }
98
+ const authHeaders = token => ({ authorization: `Bearer ${token}`, "x-prontosite-client": CLIENT_ID });
96
99
 
97
100
  /**
98
101
  * Server-Fehler → VERSTÄNDLICHE Meldung für den Chat. Zentral für ALLE Tools (über asJson). Übersetzt die
@@ -339,3 +342,47 @@ export async function sitesList(cfg, fetchImpl = fetch) {
339
342
  const { token } = resolveTarget(cfg, cfg.defaultSite || cfg.site || "active");
340
343
  return apiSitesWithToken(cfg, token, fetchImpl);
341
344
  }
345
+
346
+ // ---- Datei-API (Entwurf ohne lokale Kopie; docs/konzept-serverseitig-editieren.md §6/§7) ----
347
+ // 409 (Konflikt) NICHT als Fehler werfen, sondern komplett zurückgeben: der Assistent braucht die Liste
348
+ // „wer hat wann was geändert" (changes/intervening), um sauber neu aufzusetzen.
349
+ async function asJsonOr409(res) {
350
+ if (res.status === 409) { try { return await res.json(); } catch { return { conflict: true, error: "Konflikt (409)." }; } }
351
+ return asJson(res);
352
+ }
353
+ /** GET /api/files/<site> – Dateiliste des Entwurfs (Pfad, Größe, Typ, bearbeitbar) + draftRev. */
354
+ export async function apiFiles(cfg, { site } = {}, fetchImpl = fetch) {
355
+ const { token, slug } = resolveTarget(cfg, site);
356
+ return asJson(await fetchImpl(`${baseUrl(cfg)}/files/${encodeURIComponent(slug)}`, { headers: authHeaders(token) }));
357
+ }
358
+ /** GET /api/file/<site>?path= – Quelltext einer Datei (Text bzw. Base64) + draftRev + Overlay-Hinweis. */
359
+ export async function apiFileGet(cfg, { site, path: rel } = {}, fetchImpl = fetch) {
360
+ const { token, slug } = resolveTarget(cfg, site);
361
+ return asJson(await fetchImpl(`${baseUrl(cfg)}/file/${encodeURIComponent(slug)}?path=${encodeURIComponent(rel)}`, { headers: authHeaders(token) }));
362
+ }
363
+ /** PUT /api/file/<site>?path= – Text-Inhalt ersetzen (Entwurf). baseRev Pflicht. */
364
+ export async function apiFilePut(cfg, { site, path: rel, content, baseRev, overlay = "keep" } = {}, fetchImpl = fetch) {
365
+ const { token, slug } = resolveTarget(cfg, site);
366
+ return asJsonOr409(await fetchImpl(`${baseUrl(cfg)}/file/${encodeURIComponent(slug)}?path=${encodeURIComponent(rel)}`, {
367
+ method: "PUT", headers: { ...authHeaders(token), "content-type": "application/json" }, body: JSON.stringify({ content, baseRev, overlay }) }));
368
+ }
369
+ /** POST /api/file/<site>/delete – Datei im Entwurf als gelöscht markieren (undo-fähig). */
370
+ export async function apiFileDelete(cfg, { site, path: rel, baseRev, confirmDestructive = false } = {}, fetchImpl = fetch) {
371
+ const { token, slug } = resolveTarget(cfg, site);
372
+ return asJsonOr409(await fetchImpl(`${baseUrl(cfg)}/file/${encodeURIComponent(slug)}/delete`, {
373
+ method: "POST", headers: { ...authHeaders(token), "content-type": "application/json" }, body: JSON.stringify({ path: rel, baseRev, confirmDestructive }) }));
374
+ }
375
+ /** POST /api/file/<site>/rename – Datei umbenennen (Verweise werden NICHT umgeschrieben). */
376
+ export async function apiFileRename(cfg, { site, from, to, baseRev } = {}, fetchImpl = fetch) {
377
+ const { token, slug } = resolveTarget(cfg, site);
378
+ return asJsonOr409(await fetchImpl(`${baseUrl(cfg)}/file/${encodeURIComponent(slug)}/rename`, {
379
+ method: "POST", headers: { ...authHeaders(token), "content-type": "application/json" }, body: JSON.stringify({ from, to, baseRev }) }));
380
+ }
381
+
382
+ /** GET/POST /api/lock/<site> – sanfte Sperre („in Bearbeitung durch …"); blockiert niemanden. action: lock|unlock|takeover. */
383
+ export async function apiLock(cfg, { site, action = null, note = null } = {}, fetchImpl = fetch) {
384
+ const { token, slug } = resolveTarget(cfg, site);
385
+ const url = `${baseUrl(cfg)}/lock/${encodeURIComponent(slug)}`;
386
+ if (!action) return asJson(await fetchImpl(url, { headers: authHeaders(token) }));
387
+ return asJsonOr409(await fetchImpl(url, { method: "POST", headers: { ...authHeaders(token), "content-type": "application/json" }, body: JSON.stringify({ action, note }) }));
388
+ }
package/index.mjs CHANGED
@@ -13,13 +13,13 @@ import { realpathSync } from "node:fs";
13
13
  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
- import { loadConfig, apiStatus, apiExport, apiDeploy, apiPublish, sitesList, apiAccountCreate, apiCompanyGet, apiCompanySet, apiLegalOptions, apiDomainStatus, apiDomainConnect, apiEmbeds, apiEmbedConsent, apiWidgets, apiWidgetSet } from "./api.mjs";
16
+ import { loadConfig, apiStatus, apiExport, apiDeploy, apiPublish, sitesList, apiAccountCreate, apiCompanyGet, apiCompanySet, apiLegalOptions, apiDomainStatus, apiDomainConnect, apiEmbeds, apiEmbedConsent, apiWidgets, apiWidgetSet, apiFiles, apiFileGet, apiFilePut, apiFileDelete, apiFileRename, apiLock } from "./api.mjs";
17
17
 
18
18
  const cfg = loadConfig();
19
19
  // Deutliche Startwarnung bei ambiger Konfiguration (Top-Level-Token NEBEN sites-Map). Nach stderr, damit
20
20
  // es im MCP-Log/Terminal auffällt; die endgültige Auflösung (Betreuer-Token-Vorrang vs. Map) klärt sites_list.
21
21
  for (const w of (cfg.warnings || [])) process.stderr.write("[prontosite-mcp] ⚠ " + w + "\n");
22
- const server = new McpServer({ name: "prontosite", version: "0.10.3" });
22
+ const server = new McpServer({ name: "prontosite", version: "0.11.1" });
23
23
  const asText = obj => ({ content: [{ type: "text", text: JSON.stringify(obj, null, 2) }] });
24
24
  const asError = e => ({ isError: true, content: [{ type: "text", text: "Fehler: " + (e?.message || String(e)) }] });
25
25
  const SITE_DESC = "Website-Slug (z. B. 'mas-monti'). Ohne Angabe die Standard-Site der Config. Bei Betreuer-"
@@ -81,8 +81,9 @@ server.tool(
81
81
 
82
82
  server.tool(
83
83
  "site_status",
84
- "Status einer ProntoSite-Website: Plan, Slug, Live-URL, letzte Änderungen und offener Veröffentlichungs"
85
- + "bedarf. LESEND – ändert nichts.",
84
+ "Status einer ProntoSite-Website: Plan, Slug, Live-URL, letzte Änderungen, offener Veröffentlichungs"
85
+ + "bedarf, draftRev und recentEdits (wer hat zuletzt wo geändert). VOR Änderungen lesen – draftRev ist die baseRev für "
86
+ + "site_file_put. LESEND – ändert nichts.",
86
87
  { site: z.string().optional().describe(SITE_DESC) },
87
88
  async ({ site }) => { try { return asText(await apiStatus(cfg, { site })); } catch (e) { return asError(e); } }
88
89
  );
@@ -282,6 +283,74 @@ server.tool(
282
283
  }
283
284
  );
284
285
 
286
+ // ---- Datei-API: kleine Änderungen am Entwurf direkt auf dem Server (Konzept §7) ----
287
+ 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. ";
288
+ server.tool(
289
+ "site_files",
290
+ "LESEND: Listet die Dateien des Entwurfs einer Website (Pfad, Größe, Typ, ob bearbeitbar) und die aktuelle draftRev. "
291
+ + FILE_SMALL + "Ruf VORHER site_status auf; zeigt es kürzliche Änderungen anderer (recentEdits), sag das dem Nutzer, bevor du etwas änderst.",
292
+ { site: z.string().optional().describe(SITE_DESC) },
293
+ async ({ site }) => { try { return asText(await apiFiles(cfg, { site })); } catch (e) { return asError(e); } }
294
+ );
295
+ server.tool(
296
+ "site_file_get",
297
+ "LESEND: Liefert den Quelltext einer Datei des Entwurfs (Text; Binär als Base64, max. 5 MB) samt draftRev. "
298
+ + "Der Block overlay nennt Dashboard-Änderungen, die im Entwurf ÜBER diesem Quelltext liegen (sie gewinnen sichtbar) – "
299
+ + "berücksichtige sie, bevor du Text änderst. Merke dir draftRev: du brauchst sie als baseRev für site_file_put. " + FILE_SMALL,
300
+ { site: z.string().optional().describe(SITE_DESC), path: z.string().describe("Relativer Pfad, z. B. 'index.html' oder 'css/style.css' (aus site_files).") },
301
+ async ({ site, path }) => { try { return asText(await apiFileGet(cfg, { site, path })); } catch (e) { return asError(e); } }
302
+ );
303
+ server.tool(
304
+ "site_file_put",
305
+ "SCHREIBEND (Entwurf, nicht live; nur Betreuer-Token): Ersetzt den Inhalt einer Text-Datei (HTML einer bestehenden Seite, "
306
+ + "CSS, SVG, JSON, TXT …). PFLICHT: baseRev = draftRev aus deinem letzten site_file_get/site_status. Antwortet der Server mit "
307
+ + "conflict:true (409), hat jemand anderes dieselbe Datei inzwischen geändert: NICHT erzwingen – Datei neu holen, Änderung neu "
308
+ + "anwenden, mit der neuen draftRev erneut senden; die Antwort listet wer/wann/was. Prüfe danach overlay (umgehängte/deaktivierte "
309
+ + "Dashboard-Änderungen), sanitized und warnings und berichte sie dem Nutzer. Rechtstexte (Impressum/Datenschutz) nicht "
310
+ + "umformulieren – der Server lehnt das ab. Danach: site_status zeigt publishNeeded, live erst mit site_publish (vorher "
311
+ + "bestätigen lassen). " + FILE_SMALL,
312
+ { site: z.string().optional().describe(SITE_DESC),
313
+ path: z.string().describe("Relativer Pfad der Datei (bestehende Seite oder Text-Asset)."),
314
+ content: z.string().describe("Vollständiger neuer Dateiinhalt (Text, UTF-8, max. 1 MB)."),
315
+ baseRev: z.number().int().describe("draftRev, auf der deine Änderung beruht (aus site_file_get/site_status)."),
316
+ overlay: z.enum(["keep", "yield"]).default("keep").describe("keep = Dashboard-Änderungen bleiben über dem Quelltext; yield = wo deine Datei denselben Inhalt geändert hat, gewinnt die Datei.") },
317
+ async ({ site, path, content, baseRev, overlay }) => { try { return asText(await apiFilePut(cfg, { site, path, content, baseRev, overlay })); } catch (e) { return asError(e); } }
318
+ );
319
+ server.tool(
320
+ "site_file_delete",
321
+ "SCHREIBEND (Entwurf; nur Betreuer-Token): Markiert eine Text-Datei im Entwurf als gelöscht (rückgängig machbar; Seiten "
322
+ + "löschen geht über das Dashboard). baseRev Pflicht, confirmDestructive:true erst NACH Rückfrage beim Nutzer. Prüfe vorher, "
323
+ + "ob die Datei noch verlinkt ist. " + FILE_SMALL,
324
+ { site: z.string().optional().describe(SITE_DESC), path: z.string(), baseRev: z.number().int(),
325
+ confirmDestructive: z.boolean().default(false).describe("true erst nach ausdrücklicher Bestätigung durch den Nutzer.") },
326
+ async ({ site, path, baseRev, confirmDestructive }) => { try { return asText(await apiFileDelete(cfg, { site, path, baseRev, confirmDestructive })); } catch (e) { return asError(e); } }
327
+ );
328
+ server.tool(
329
+ "site_file_rename",
330
+ "SCHREIBEND (Entwurf; nur Betreuer-Token): Benennt eine Text-Datei (kein HTML) um. Verweise in anderen Dateien werden NICHT "
331
+ + "angepasst – die musst du selbst per site_file_put nachziehen. baseRev Pflicht. " + FILE_SMALL,
332
+ { site: z.string().optional().describe(SITE_DESC), from: z.string(), to: z.string(), baseRev: z.number().int() },
333
+ async ({ site, from, to, baseRev }) => { try { return asText(await apiFileRename(cfg, { site, from, to, baseRev })); } catch (e) { return asError(e); } }
334
+ );
335
+
336
+ server.tool(
337
+ "site_lock",
338
+ "Setzt eine sanfte Sperre „in Bearbeitung durch <dich>“ (2 h, verlängert sich bei jeder deiner Änderungen). Blockiert "
339
+ + "niemanden – andere sehen nur einen Hinweis und bekommen beim Speichern eine Warnung. Vor einer längeren Bearbeitungsserie "
340
+ + "setzen. Ist schon jemand anderes in Bearbeitung, kommt conflict/409 mit dem Hinweis – takeover:true NUR als Betreuer und nur "
341
+ + "nach Rückfrage beim Nutzer (die andere Person sieht die Übernahme).",
342
+ { site: z.string().optional().describe(SITE_DESC),
343
+ note: z.string().optional().describe("Optionale Notiz, z. B. „Startseite überarbeiten“ (andere sehen sie im Hinweis)."),
344
+ takeover: z.boolean().default(false).describe("true = fremde Sperre übernehmen (nur Betreuer, nach Rückfrage).") },
345
+ async ({ site, note, takeover }) => { try { return asText(await apiLock(cfg, { site, note, action: takeover ? "takeover" : "lock" })); } catch (e) { return asError(e); } }
346
+ );
347
+ server.tool(
348
+ "site_unlock",
349
+ "Gibt deine sanfte Sperre („in Bearbeitung“) frei – am Ende der Bearbeitung aufrufen. Ohne Sperre ein No-op.",
350
+ { site: z.string().optional().describe(SITE_DESC) },
351
+ async ({ site }) => { try { return asText(await apiLock(cfg, { site, action: "unlock" })); } catch (e) { return asError(e); } }
352
+ );
353
+
285
354
  // Den stdio-Server NUR starten, wenn diese Datei direkt ausgeführt wird (als MCP-Server / bin).
286
355
  // Beim bloßen Importieren (Smoke-Test / Tests) NICHT verbinden – sonst würde der Import auf stdin blockieren.
287
356
  // So lädt der Import die Datei komplett (inkl. aller server.tool-Registrierungen → fängt Syntax-/Beschreibungs-
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@prontosite/mcp",
3
- "version": "0.10.3",
3
+ "version": "0.11.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": {