@prontosite/mcp 0.9.0 → 0.9.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 CHANGED
@@ -3,6 +3,15 @@
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.9.2 – 2026-10-01
7
+
8
+ - **Pflicht-Marker in den Tool-Beschreibungen** (`site_deploy`, `site_legal_options`) + README: Rechtsseiten
9
+ bringen `data-ps="impressum"`/`"datenschutz"` + eine EIGENE `<h1>` mit (kein handgeschriebener Rechtstext) →
10
+ `site_legal_options` mit `showTitle:false` setzen (bei `?markers=auto` automatisch bei erkannter eigener H1);
11
+ Footer IMMER mit allen vier Markern (`link-impressum`, `link-datenschutz`, `cookie-einstellungen`,
12
+ `cookie-richtlinie`), sonst ungestylter Consent-Block; Mobile-Menü rendert als Drawer, Toggle braucht
13
+ `aria-controls`. Reine Beschreibungs-/Doku-Änderung (keine neue Funktion).
14
+
6
15
  ## 0.9.0 – 2026-09-27
7
16
 
8
17
  - **Neu: `site_widgets`** (lesend) – Katalog der verwalteten Widgets einer Website inkl. `aktiv`, `optionen`
package/README.md CHANGED
@@ -99,6 +99,21 @@ sichern: `cp ~/.prontosite.json ~/.prontosite.json.bak`).
99
99
  `site_deploy` und `site_publish` sind **schreibende** Aktionen (verändern die Live-Website). Die
100
100
  Tool-Beschreibungen weisen den Assistenten an, vorher rückzufragen. Jeder Aufruf wird serverseitig auditiert.
101
101
 
102
+ ### Pflicht-Marker beim lokalen Bauen
103
+
104
+ Baust du das Template selbst, gehören drei Dinge dazu (volle Spec:
105
+ [Marker-Syntax](https://prontosite.io/docs/marker-shortcodes)):
106
+
107
+ - **Rechtsseiten:** `impressum/index.html` und `datenschutz/index.html` tragen IMMER
108
+ `<div data-ps="impressum">` bzw. `<div data-ps="datenschutz">` als Inhaltsbereich und eine **eigene `<h1>`**
109
+ (kein handgeschriebener Rechtstext). Danach `site_legal_options({ impressum:{ showTitle:false } })` setzen
110
+ (eigene H1 → keine doppelte Überschrift). Beim Erst-Import mit `?markers=auto`/der Rechts-Automatik setzt
111
+ ProntoSite `showTitle:false` automatisch, sobald die Rechtsseite eine eigene H1 hat.
112
+ - **Footer:** IMMER alle vier Marker `link-impressum`, `link-datenschutz`, `cookie-einstellungen`,
113
+ `cookie-richtlinie` – sonst hängt das Consent-Widget einen ungestylten Block an den Seitenfuß (mehrsprachig
114
+ je Sprach-Footer dieselben Marker, Linktext lokalisiert).
115
+ - **Mobile-Menü:** rendert als Drawer (von rechts); der Toggle braucht `aria-controls` auf das Menü.
116
+
102
117
  ## Arbeitsweise: ein Chat je Kunde
103
118
 
104
119
  Für saubere Trennung: pro Kundenwebsite **einen eigenen Claude-Chat**. So bezieht sich der ganze Verlauf auf
package/api.mjs CHANGED
@@ -69,7 +69,9 @@ export function resolveTarget(cfg, site) {
69
69
  if (!key) throw new Error(`„${label}" ist kein Profil in deiner sites-Map. Verfügbar: ${keys.join(", ")}.`);
70
70
  return { token: needToken(cfg.sites[key].token, key), slug: "new" };
71
71
  }
72
- return { token: needToken(cfg.token, "new"), slug: "new" };
72
+ // Einzel-/Betreuer-Token: „<Konto-ID>:new" trägt die Ziel-Konto-ID (k-…) im Label → als customer
73
+ // durchreichen (Betreuer-Erst-Import ohne separaten customer-Parameter). Bare „new" hat keine.
74
+ return { token: needToken(cfg.token, "new"), slug: "new", customer: newMatch ? newMatch[1] : null };
73
75
  }
74
76
  if (cfg.sites) {
75
77
  const keys = Object.keys(cfg.sites);
@@ -125,20 +127,36 @@ export async function apiExport(cfg, { site, which = "published", outDir = "." }
125
127
  /** POST /api/deploy/<site> – ZIP übergeben (+ optional publish/resetOverlay/legal). site="new" = anlegen. */
126
128
  export async function apiDeploy(cfg, { site, customer = null, slug: wishSlug = null, zipPath, publish = false, resetOverlay = false, legal = null, company = null, siteOverride = false } = {}, fetchImpl = fetch) {
127
129
  if (!zipPath || !fs.existsSync(zipPath)) throw new Error(`ZIP nicht gefunden: ${zipPath}`);
128
- // Optional: Firmendaten VOR dem Deploy setzen (ein Schritt) → Rechtstext-Automatik nutzt sie direkt.
129
- if (company && typeof company === "object") await apiCompanySet(cfg, { site, company, siteOverride }, fetchImpl);
130
- const { token, slug } = resolveTarget(cfg, site);
130
+ const target = resolveTarget(cfg, site);
131
+ const { token, slug } = target;
132
+ const isNew = slug === "new";
133
+ const effCustomer = customer || target.customer || null; // „<Konto-ID>:new" liefert customer; sonst Parameter
134
+ const hasCompany = company && typeof company === "object";
135
+ // Bestandssite: Firmendaten VOR dem Deploy (Rechtstext-Automatik nutzt sie sofort). ERST-Import: NICHT vorab –
136
+ // die Website existiert noch nicht (company-/legal-Gate → 404); Firmendaten kommen NACH dem Anlegen (unten).
137
+ if (hasCompany && !isNew) await apiCompanySet(cfg, { site, company, siteOverride }, fetchImpl);
131
138
  const q = new URLSearchParams();
132
139
  if (publish) q.set("publish", "1");
133
140
  if (resetOverlay) q.set("resetOverlay", "1");
134
141
  if (legal === true) q.set("legal", "1");
135
142
  if (legal === false) q.set("legal", "0");
136
- if (customer) q.set("customer", customer); // Betreuer-Token + site="new": Ziel-Konto für den Erst-Import
143
+ if (effCustomer) q.set("customer", effCustomer); // Betreuer-Token + „new": Ziel-Konto für den Erst-Import
137
144
  if (wishSlug) q.set("slug", String(wishSlug)); // Erst-Import: Wunsch-Adresse (sonst wird sie abgeleitet)
138
145
  const qs = q.toString() ? `?${q}` : "";
139
146
  const res = await fetchImpl(`${baseUrl(cfg)}/deploy/${encodeURIComponent(slug)}${qs}`, {
140
147
  method: "POST", headers: { ...authHeaders(token), "content-type": "application/zip" }, body: fs.readFileSync(zipPath) });
141
- return asJson(res);
148
+ const out = await asJson(res);
149
+ // ERST-Import + Firmendaten: jetzt auf die ERSTELLTE Website setzen (out.slug/out.site). Hinweis: die beim
150
+ // Erst-Import automatisch erzeugten Rechtstexte kennen diese Daten noch nicht – ein erneuter Rechts-Lauf/
151
+ // Publish übernimmt sie. Das melden wir als companyApplied/companyNote statt still zu lassen.
152
+ if (hasCompany && isNew && out && (out.slug || out.site)) {
153
+ try {
154
+ await apiCompanySet(cfg, { site: out.slug || out.site, company, siteOverride }, fetchImpl);
155
+ out.companyApplied = true;
156
+ if (out.legal && !out.legal.disabled) out.companyNote = "Firmendaten wurden NACH dem Erst-Import gesetzt – die automatisch erzeugten Rechtstexte übernehmen sie erst beim nächsten Rechts-Lauf/Publish.";
157
+ } catch (e) { out.companyApplied = false; out.companyError = e.message; }
158
+ }
159
+ return out;
142
160
  }
143
161
 
144
162
  /** POST /api/publish/<site> – erneut veröffentlichen (ohne ZIP). force=true = „trotzdem veröffentlichen"
@@ -246,9 +264,10 @@ export async function apiLegalOptions(cfg, { site, impressum, datenschutz } = {}
246
264
  // GET /api/sites mit EINEM Token → normalisierte Liste (Betreuer-Token: alle betreuten Kunden).
247
265
  async function apiSitesWithToken(cfg, token, fetchImpl) {
248
266
  const body = await asJson(await fetchImpl(`${baseUrl(cfg)}/sites`, { headers: authHeaders(token) }));
249
- const sites = (body.sites || []).map(s => ({ label: s.slug || s.site, slug: s.slug || null, id: s.site || null,
267
+ const sites = (body.sites || []).map(s => ({ label: s.slug || s.site || s.customer, slug: s.slug || null, id: s.site || null,
250
268
  name: s.name || null, plan: s.plan, everPublished: s.everPublished, publishNeeded: s.publishNeeded,
251
- lastChangeAt: s.lastChangeAt || null, liveUrl: s.liveUrl || null, customer: s.customer }));
269
+ lastChangeAt: s.lastChangeAt || null, liveUrl: s.liveUrl || null, customer: s.customer,
270
+ ...(s.needsFirstImport ? { needsFirstImport: true, hint: s.hint || null } : {}) }));
252
271
  return { count: sites.length, manager: !!body.manager, hint: body.hint || null, sites };
253
272
  }
254
273
  // sites-Map abfragen: jeden Eintrag einzeln (jeder Token → seine Site).
package/index.mjs CHANGED
@@ -17,7 +17,7 @@ const cfg = loadConfig();
17
17
  // Deutliche Startwarnung bei ambiger Konfiguration (Top-Level-Token NEBEN sites-Map). Nach stderr, damit
18
18
  // es im MCP-Log/Terminal auffällt; die endgültige Auflösung (Betreuer-Token-Vorrang vs. Map) klärt sites_list.
19
19
  for (const w of (cfg.warnings || [])) process.stderr.write("[prontosite-mcp] ⚠ " + w + "\n");
20
- const server = new McpServer({ name: "prontosite", version: "0.9.0" });
20
+ const server = new McpServer({ name: "prontosite", version: "0.9.1" });
21
21
  const asText = obj => ({ content: [{ type: "text", text: JSON.stringify(obj, null, 2) }] });
22
22
  const asError = e => ({ isError: true, content: [{ type: "text", text: "Fehler: " + (e?.message || String(e)) }] });
23
23
  const SITE_DESC = "Website-Slug (z. B. 'mas-monti'). Ohne Angabe die Standard-Site der Config. Bei Betreuer-"
@@ -85,10 +85,17 @@ server.tool(
85
85
  + "erscheinen sollen – data-ps=\"formular:kontakt\" (verwaltetes Kontaktformular), data-ps=\"impressum\" bzw. "
86
86
  + "data-ps=\"datenschutz\" (Rechtstext-Seite) und data-ps=\"link-impressum\" / data-ps=\"link-datenschutz\" "
87
87
  + "(Footer-Links auf die Rechtstexte). Beim Import legt ProntoSite die Bausteine automatisch an. "
88
+ + "PFLICHT-MARKER: (a) Rechtsseiten impressum/index.html + datenschutz/index.html tragen IMMER den "
89
+ + "data-ps-Marker als Inhaltsbereich UND eine EIGENE <h1>; schreib KEINEN Rechtstext von Hand hinein. Weil "
90
+ + "die Seite ihre eigene H1 hat, die Widget-Überschrift ausschalten – site_legal_options mit showTitle:false "
91
+ + "(bei ?markers=auto setzt ProntoSite das automatisch, sobald die Rechtsseite eine eigene H1 hat). "
92
+ + "(b) Footer IMMER mit allen vier Markern link-impressum, link-datenschutz, cookie-einstellungen, "
93
+ + "cookie-richtlinie (sonst hängt das Consent-Widget einen ungestylten Block an den Seitenfuß; mehrsprachig "
94
+ + "je Sprach-Footer dieselben Marker). (c) Mobile-Menü-Toggle braucht aria-controls (Menü rendert als Drawer). "
88
95
  + "Syntax-Referenz: https://prontosite.io/docs/marker-shortcodes",
89
- { site: z.string().optional().describe(SITE_DESC + " Erst-Import: 'new' (Einzel) bzw. '<label>:new' (Multi-Site)."),
96
+ { site: z.string().optional().describe(SITE_DESC + " Erst-Import: 'new' (Einzel) bzw. '<label>:new' (Multi-Site). Betreuer-Token: '<Konto-ID>:new' (k-…) legt im betreuten Konto an – die Konto-ID im Label ersetzt den customer-Parameter. sites_list zeigt betreute Konten OHNE Website (needsFirstImport) samt ihrer k-ID."),
90
97
  slug: z.string().optional().describe("Erst-Import: gewünschte Web-Adresse (Subdomain-Label, z. B. 'wuerttemberg' → wuerttemberg.prontosite.io). Beim Erst-Import den Nutzer danach FRAGEN; wird sie weggelassen, leitet ProntoSite eine Adresse ab. Bei Updates ignoriert."),
91
- customer: z.string().optional().describe("Nur Betreuer-Token + Erst-Import mit EINEM Manager-Token: Ziel-Konto-ID (k-…). Bei per-Konto-Token in der sites-Map genügt '<label>:new'."),
98
+ 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'."),
92
99
  zipPath: z.string().describe("Pfad zur ZIP-Datei der Website (lokal)."),
93
100
  publish: z.boolean().default(false).describe("true = nach dem Import direkt veröffentlichen (LIVE)."),
94
101
  resetOverlay: z.boolean().default(false).describe("true = bisherige Dashboard-Änderungen verwerfen."),
@@ -135,7 +142,7 @@ server.tool(
135
142
  );
136
143
 
137
144
  const legalOptShape = z.object({
138
- showTitle: z.boolean().optional().describe("Überschrift (H1) des Rechtstextes anzeigen (Default an)."),
145
+ showTitle: z.boolean().optional().describe("Widget-Überschrift des Rechtstextes anzeigen (Default an). Bringt die Rechtsseite ihre EIGENE <h1> mit (Pflicht-Marker-Konvention), hier showTitle:false setzen, sonst doppelte Überschrift."),
139
146
  showDate: z.boolean().optional().describe("Stand-Datum anzeigen (Default an; nur wo vorhanden, i. d. R. Datenschutz)."),
140
147
  draftNotice: z.boolean().optional().describe("Entwurfs-Hinweis in der VERÖFFENTLICHTEN Seite anzeigen (Default an)."),
141
148
  }).describe("Anzeige-Optionen einer Rechtstext-Rolle – Teil-Update, nur gesetzte Booleans wirken.");
@@ -146,7 +153,9 @@ server.tool(
146
153
  + "(Überschrift), showDate (Stand-Datum), draftNotice (Entwurfs-Hinweis in der Live-Ausgabe). OHNE impressum/"
147
154
  + "datenschutz nur LESEN. Teil-Update (nur gesetzte Felder). HINWEIS: draftNotice:false blendet den rechtlich "
148
155
  + "empfohlenen Entwurfs-Hinweis live aus (protokolliert) – bitte vorher bestätigen lassen. "
149
- + "MARKER (beim lokalen Bauen): Rechtstext-Seiten und -Links im Template mit data-ps markieren – "
156
+ + "WICHTIG: Rechtsseiten sollen laut Konvention ihre EIGENE <h1> mitbringen (Marker data-ps=\"impressum\"/"
157
+ + "\"datenschutz\" als Inhaltsbereich) – dann hier showTitle:false setzen, damit keine doppelte Überschrift "
158
+ + "entsteht. MARKER (beim lokalen Bauen): Rechtstext-Seiten und -Links im Template mit data-ps markieren – "
150
159
  + "data-ps=\"impressum\" / data-ps=\"datenschutz\" (Seite) bzw. data-ps=\"link-impressum\" / "
151
160
  + "data-ps=\"link-datenschutz\" (Footer-Links). Syntax-Referenz: https://prontosite.io/docs/marker-shortcodes",
152
161
  { site: z.string().optional().describe(SITE_DESC),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@prontosite/mcp",
3
- "version": "0.9.0",
3
+ "version": "0.9.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": {