@prontosite/mcp 0.10.0 → 0.10.3

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,38 @@
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
7
+
8
+ - **`site_deploy`:** neuer Parameter `name` (Erst-Import) – setzt den Website-Namen explizit, falls die
9
+ Ableitung aus der Startseite danebenliegt (z. B. „Physio Phoenix – Bad Honnef Süd" → `name="Physio Phoenix"`).
10
+ Beschreibung ergänzt um die **Reihenfolge-Empfehlung** „Konto → Firmendaten → Import".
11
+ - **`site_company_set`:** Beschreibung klärt, dass ein Firmendaten-Set NACH einem Import die Rechts-Automatik
12
+ AUTOMATISCH neu laufen lässt (Entwurf; `legal.refreshed` in der Antwort) – ein zweiter Import ist nicht mehr
13
+ nötig. `apiDeploy` meldet das im `companyNote` entsprechend.
14
+
15
+ ## 0.10.2 – 2026-10-14
16
+
17
+ - **Verständliche Fehlermeldungen** statt roher Server-/HTTP-Fehler – zentral in `api.mjs`
18
+ (`friendlyApiError`, greift über `asJson` für JEDES Tool: `site_deploy`, `site_status`, `sites_list`, …):
19
+ - **403** mit „API-Zugang nicht im Tarif" (bzw. alt „Starter-Plan") → *„Dein ProntoSite-Tarif enthält keinen
20
+ API-Zugang. Wähle im Dashboard → Zahlung einen Tarif oder bitte deinen Betreuer um eine API-Freigabe."*
21
+ - **401** (Token ungültig/abgelaufen) → Hinweis, das Token im Dashboard → API-Zugang neu zu erstellen und in
22
+ `~/.prontosite.json` einzutragen.
23
+ - **429** → Hinweis auf das Rate-Limit (kurz warten und erneut versuchen).
24
+ Andere 403 (z. B. „Kein Zugriff auf dieses Konto") bleiben unverändert. Tests mit gemockten Antworten
25
+ (401/403/429 über mehrere Tools) in `tests/mcp-prontosite.test.js`.
26
+
27
+ ## 0.10.1 – 2026-10-03
28
+
29
+ - **Fix:** In der `account_create`-Beschreibung beendeten gerade Anführungszeichen (`"`) innerhalb der
30
+ deutschen Quotes `„…"` den JS-String vorzeitig → `SyntaxError` beim Start (0.10.0 war nicht ladbar).
31
+ Alle Tool-Beschreibungen auf typografische `„…“` umgestellt.
32
+ - **Schutz gegen Wiederholung:** `prepublishOnly` (= `npm run check` + `npm run smoke`) prüft vor jedem
33
+ Publish `node --check index.mjs api.mjs` **und** einen echten Import-Smoke (`smoke.mjs` lädt `index.mjs`
34
+ komplett – alle `server.tool`-Registrierungen laufen – ohne den stdio-Server zu starten, Dummy-Env).
35
+ Derselbe Smoke läuft in `npm test` des Hauptprojekts. `index.mjs` startet den Server jetzt nur noch bei
36
+ Direktausführung (Main-Guard), damit der Import nicht blockiert.
37
+
6
38
  ## 0.10.0 – 2026-10-03
7
39
 
8
40
  - **Neu: `account_create`** (schreibend, nur mit Betreuer-Token: admin/partner/agentur) – legt ein neues
package/README.md CHANGED
@@ -122,6 +122,20 @@ genau diese Website, und Verwechslungen sind ausgeschlossen. `sites_list` zeigt
122
122
  Chat einfach sagen „deploye mas-monti", ohne Slugs auswendig zu kennen. (Alternativ ein Chat für alles und die
123
123
  Website je Aufruf über `site` wählen – die getrennten Chats sind aber übersichtlicher.)
124
124
 
125
+ ## Fehlermeldungen
126
+
127
+ Statt roher HTTP-/Server-Fehler geben **alle** Tools bei den drei häufigen Betriebsfällen eine verständliche
128
+ Meldung zurück (zentral in `api.mjs`):
129
+
130
+ - **Kein API-Zugang im Tarif (403):** „Dein ProntoSite-Tarif enthält keinen API-Zugang. Wähle im
131
+ Dashboard → Zahlung einen Tarif oder bitte deinen Betreuer um eine API-Freigabe."
132
+ - **Token ungültig/abgelaufen (401):** „Dein API-Token ist ungültig oder abgelaufen. Token im
133
+ Dashboard → API-Zugang neu erstellen und in `~/.prontosite.json` eintragen."
134
+ - **Zu viele Anfragen (429):** Hinweis auf das Rate-Limit – einen Moment warten und erneut versuchen.
135
+
136
+ Andere Fehler (z. B. „Kein Zugriff auf dieses Konto", „Website nicht gefunden") werden unverändert mit der
137
+ Server-Meldung durchgereicht.
138
+
125
139
  ## Sicherheit
126
140
 
127
141
  - Tokens liegen nur lokal (Config/Env), werden nie geloggt und nie in Antworten zurückgegeben.
package/api.mjs CHANGED
@@ -94,11 +94,31 @@ export function resolveTarget(cfg, site) {
94
94
  const baseUrl = cfg => `https://${cfg.host}/api`;
95
95
  const authHeaders = token => ({ authorization: `Bearer ${token}` });
96
96
 
97
+ /**
98
+ * Server-Fehler → VERSTÄNDLICHE Meldung für den Chat. Zentral für ALLE Tools (über asJson). Übersetzt die
99
+ * drei häufigen Betriebsfälle; alles andere behält die Server-Meldung (bzw. „HTTP <status>"). Token werden
100
+ * nie geloggt/zurückgegeben – hier sind ohnehin nur Status + Server-Text im Spiel.
101
+ * 401 Token ungültig/abgelaufen → neues Token erstellen + in ~/.prontosite.json eintragen.
102
+ * 403 API-Gate (Text „API-Zugang nicht im Tarif" bzw. „Starter-Plan") → Tarif ohne API-Zugang.
103
+ * 429 Rate-Limit → kurz warten.
104
+ * Andere 403 (z. B. „Kein Zugriff auf dieses Konto") bleiben unverändert – nur das API-Zugangs-Gate wird gemappt.
105
+ */
106
+ export function friendlyApiError(status, serverError = "") {
107
+ const raw = String(serverError || "").trim();
108
+ if (status === 401)
109
+ return "Dein API-Token ist ungültig oder abgelaufen. Token im Dashboard → API-Zugang neu erstellen und in ~/.prontosite.json eintragen.";
110
+ if (status === 403 && /API-Zugang nicht im Tarif|Starter-Plan/i.test(raw))
111
+ return "Dein ProntoSite-Tarif enthält keinen API-Zugang. Wähle im Dashboard → Zahlung einen Tarif oder bitte deinen Betreuer um eine API-Freigabe.";
112
+ if (status === 429)
113
+ return "Zu viele Anfragen in kurzer Zeit (Rate-Limit). Bitte einen Moment warten und erneut versuchen.";
114
+ return raw || `HTTP ${status}`;
115
+ }
116
+
97
117
  /** Antwort als JSON lesen; bei Fehlerstatus eine sprechende Exception (ohne Token). */
98
118
  async function asJson(res) {
99
119
  let body = {};
100
120
  try { body = await res.json(); } catch { /* kein JSON */ }
101
- if (!res.ok) throw new Error(body.error || `HTTP ${res.status}`);
121
+ if (!res.ok) throw new Error(friendlyApiError(res.status, body.error));
102
122
  return body;
103
123
  }
104
124
 
@@ -125,7 +145,7 @@ export async function apiExport(cfg, { site, which = "published", outDir = "." }
125
145
  }
126
146
 
127
147
  /** POST /api/deploy/<site> – ZIP übergeben (+ optional publish/resetOverlay/legal). site="new" = anlegen. */
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) {
148
+ 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) {
129
149
  if (!zipPath || !fs.existsSync(zipPath)) throw new Error(`ZIP nicht gefunden: ${zipPath}`);
130
150
  const target = resolveTarget(cfg, site);
131
151
  const { token, slug } = target;
@@ -142,6 +162,7 @@ export async function apiDeploy(cfg, { site, customer = null, slug: wishSlug = n
142
162
  if (legal === false) q.set("legal", "0");
143
163
  if (effCustomer) q.set("customer", effCustomer); // Betreuer-Token + „new": Ziel-Konto für den Erst-Import
144
164
  if (wishSlug) q.set("slug", String(wishSlug)); // Erst-Import: Wunsch-Adresse (sonst wird sie abgeleitet)
165
+ if (isNew && name) q.set("name", String(name)); // Erst-Import: Site-Name explizit setzen (sonst aus der Startseite abgeleitet)
145
166
  const qs = q.toString() ? `?${q}` : "";
146
167
  const res = await fetchImpl(`${baseUrl(cfg)}/deploy/${encodeURIComponent(slug)}${qs}`, {
147
168
  method: "POST", headers: { ...authHeaders(token), "content-type": "application/zip" }, body: fs.readFileSync(zipPath) });
@@ -151,9 +172,11 @@ export async function apiDeploy(cfg, { site, customer = null, slug: wishSlug = n
151
172
  // Publish übernimmt sie. Das melden wir als companyApplied/companyNote statt still zu lassen.
152
173
  if (hasCompany && isNew && out && (out.slug || out.site)) {
153
174
  try {
154
- await apiCompanySet(cfg, { site: out.slug || out.site, company, siteOverride }, fetchImpl);
175
+ const compRes = await apiCompanySet(cfg, { site: out.slug || out.site, company, siteOverride }, fetchImpl);
155
176
  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.";
177
+ // Firmendaten-Set lässt serverseitig die Rechts-Automatik neu laufen (Entwurf) – kein zweiter Import nötig.
178
+ if (compRes && compRes.legal && compRes.legal.refreshed && compRes.legal.refreshed.length)
179
+ out.companyNote = `Firmendaten gesetzt – Rechtstexte (${compRes.legal.refreshed.join(", ")}) automatisch neu erzeugt (Entwurf). Zum Live-Schalten veröffentlichen.`;
157
180
  } catch (e) { out.companyApplied = false; out.companyError = e.message; }
158
181
  }
159
182
  return out;
package/index.mjs CHANGED
@@ -8,6 +8,8 @@
8
8
  * Multi-Site (Betreuer): trägt die Config eine `sites`-Map, adressiert jedes Tool per `site`-Parameter die
9
9
  * jeweilige Website (eigenes Token je Site). Arbeitsweise: EIN Chat je Kunde. `sites_list` zeigt alle Sites.
10
10
  */
11
+ import { fileURLToPath } from "node:url";
12
+ import { realpathSync } from "node:fs";
11
13
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
12
14
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
13
15
  import { z } from "zod";
@@ -17,7 +19,7 @@ const cfg = loadConfig();
17
19
  // Deutliche Startwarnung bei ambiger Konfiguration (Top-Level-Token NEBEN sites-Map). Nach stderr, damit
18
20
  // es im MCP-Log/Terminal auffällt; die endgültige Auflösung (Betreuer-Token-Vorrang vs. Map) klärt sites_list.
19
21
  for (const w of (cfg.warnings || [])) process.stderr.write("[prontosite-mcp] ⚠ " + w + "\n");
20
- const server = new McpServer({ name: "prontosite", version: "0.10.0" });
22
+ const server = new McpServer({ name: "prontosite", version: "0.10.3" });
21
23
  const asText = obj => ({ content: [{ type: "text", text: JSON.stringify(obj, null, 2) }] });
22
24
  const asError = e => ({ isError: true, content: [{ type: "text", text: "Fehler: " + (e?.message || String(e)) }] });
23
25
  const SITE_DESC = "Website-Slug (z. B. 'mas-monti'). Ohne Angabe die Standard-Site der Config. Bei Betreuer-"
@@ -57,9 +59,9 @@ server.tool(
57
59
  server.tool(
58
60
  "account_create",
59
61
  "SCHREIBEND (nur mit Betreuer-Token: admin/partner/agentur): Legt ein NEUES Kundenkonto an – der Dashboard-"
60
- + "Weg „Kunde anlegen". Nutze dieses Tool, BEVOR du eine Website importierst, wenn es für den Kunden noch "
62
+ + "Weg „Kunde anlegen“. Nutze dieses Tool, BEVOR du eine Website importierst, wenn es für den Kunden noch "
61
63
  + "KEIN Konto gibt (z. B. sites_list zeigt den Kunden nicht, oder der Nutzer sagt „für Kunde X gibt es noch "
62
- + "kein Konto"). Erst Konto anlegen, dann mit der zurückgegebenen Konto-ID (k-…) per site_deploy und "
64
+ + "kein Konto“). Erst Konto anlegen, dann mit der zurückgegebenen Konto-ID (k-…) per site_deploy und "
63
65
  + "site=\"<k-id>:new\" die erste Website importieren. Richte KEIN site=new/Erst-Import ein, solange die "
64
66
  + "Konto-ID unbekannt ist – dann zuerst dieses Tool. Mit email wird ein Login-Konto angelegt und eine "
65
67
  + "Einladungs-Mail verschickt; ohne email ein verwaltetes Konto (ohne Login, Betrieb über den Betreuer). "
@@ -104,6 +106,11 @@ server.tool(
104
106
  + "(„Unter welcher Adresse soll die Website laufen?“, z. B. 'wuerttemberg' → wuerttemberg.prontosite.io) "
105
107
  + "und übergib sie als slug – ohne slug wird die Adresse nur aus Konto-/Seitenname ABGELEITET (die Antwort "
106
108
  + "nennt dann die abgeleitete Adresse; sie ist später änderbar). Bitte VOR dem Aufruf bestätigen lassen. "
109
+ + "REIHENFOLGE beim Erst-Import: zuerst Konto (account_create) → Firmendaten (company hier mitgeben ODER "
110
+ + "site_company_set) → Import; so stehen Impressum/Datenschutz sofort korrekt (ein Firmendaten-Set nach dem "
111
+ + "Import lässt die Rechts-Automatik automatisch neu laufen – kein zweiter Import nötig). "
112
+ + "WEBSITE-NAME: wird aus der Startseite abgeleitet (og:site_name → Titel-Präfix vor dem ersten Trenner → "
113
+ + "Header-H1/Logo-alt); mit name=… explizit setzen (Betreuer), falls die Ableitung danebenliegt. "
107
114
  + "MARKER (wenn du das Template lokal baust): markiere im HTML mit dem Attribut data-ps, wo Bausteine "
108
115
  + "erscheinen sollen – data-ps=\"formular:kontakt\" (verwaltetes Kontaktformular), data-ps=\"impressum\" bzw. "
109
116
  + "data-ps=\"datenschutz\" (Rechtstext-Seite) und data-ps=\"link-impressum\" / data-ps=\"link-datenschutz\" "
@@ -118,6 +125,7 @@ server.tool(
118
125
  + "Syntax-Referenz: https://prontosite.io/docs/marker-shortcodes",
119
126
  { 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."),
120
127
  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."),
128
+ 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."),
121
129
  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'."),
122
130
  zipPath: z.string().describe("Pfad zur ZIP-Datei der Website (lokal)."),
123
131
  publish: z.boolean().default(false).describe("true = nach dem Import direkt veröffentlichen (LIVE)."),
@@ -125,8 +133,8 @@ server.tool(
125
133
  legal: z.boolean().optional().describe("Rechts-Automatik (Impressum/Datenschutz): true erzwingt, false schaltet ab."),
126
134
  company: companyObject.optional().describe("Optional: Firmendaten VOR dem Deploy setzen (ein Schritt) – Impressum/Datenschutz nutzen sie dann direkt. Teil-Update."),
127
135
  siteOverride: z.boolean().default(false).describe("Nur mit company: true = Firmendaten NUR für diese Website (statt konto-weit).") },
128
- async ({ site, slug, customer, zipPath, publish, resetOverlay, legal, company, siteOverride }) => {
129
- try { return asText(await apiDeploy(cfg, { site, slug, customer, zipPath, publish, resetOverlay, legal, company, siteOverride })); } catch (e) { return asError(e); }
136
+ async ({ site, slug, name, customer, zipPath, publish, resetOverlay, legal, company, siteOverride }) => {
137
+ try { return asText(await apiDeploy(cfg, { site, slug, name, customer, zipPath, publish, resetOverlay, legal, company, siteOverride })); } catch (e) { return asError(e); }
130
138
  }
131
139
  );
132
140
 
@@ -154,8 +162,11 @@ server.tool(
154
162
  + "USt-IdNr., Register, Kammer/Beruf, Aufsicht, § 18 MStV, Social). Diese Daten füllen beim nächsten Deploy "
155
163
  + "automatisch Impressum & Datenschutz. TEIL-UPDATE: nur die übergebenen Felder ändern sich. Du darfst die "
156
164
  + "Angaben aus jeder Vorlage extrahieren (Impressum-Text, Visitenkarte, Freitext) und strukturiert übergeben. "
157
- + "Default konto-weit; siteOverride=true nur, wenn DIESE eine Website abweichende Angaben braucht. Bitte VOR "
158
- + "dem Speichern die erkannten Felder kurz zusammenfassen und bestätigen lassen.",
165
+ + "Default konto-weit; siteOverride=true nur, wenn DIESE eine Website abweichende Angaben braucht. NACH EINEM "
166
+ + "IMPORT lässt dieser Aufruf die Rechts-Automatik AUTOMATISCH neu laufen (Entwurf) – Impressum/Datenschutz "
167
+ + "übernehmen die Firmendaten sofort, ein zweiter Import ist NICHT nötig (die Antwort nennt die neu erzeugten "
168
+ + "Rechtstexte unter legal.refreshed); danach site_publish zum Live-Schalten. Bitte VOR dem Speichern die "
169
+ + "erkannten Felder kurz zusammenfassen und bestätigen lassen.",
159
170
  { site: z.string().optional().describe(SITE_DESC),
160
171
  company: companyObject,
161
172
  siteOverride: z.boolean().default(false).describe("true = Firmendaten NUR für diese Website (statt konto-weit).") },
@@ -271,5 +282,13 @@ server.tool(
271
282
  }
272
283
  );
273
284
 
274
- const transport = new StdioServerTransport();
275
- await server.connect(transport);
285
+ // Den stdio-Server NUR starten, wenn diese Datei direkt ausgeführt wird (als MCP-Server / bin).
286
+ // Beim bloßen Importieren (Smoke-Test / Tests) NICHT verbinden – sonst würde der Import auf stdin blockieren.
287
+ // So lädt der Import die Datei komplett (inkl. aller server.tool-Registrierungen → fängt Syntax-/Beschreibungs-
288
+ // fehler) ohne den Server zu starten.
289
+ let isMain = false;
290
+ try { isMain = !!process.argv[1] && realpathSync(process.argv[1]) === fileURLToPath(import.meta.url); } catch { /* argv[1] fehlt (node -e) → kein Main */ }
291
+ if (isMain) {
292
+ const transport = new StdioServerTransport();
293
+ await server.connect(transport);
294
+ }
package/package.json CHANGED
@@ -1,12 +1,17 @@
1
1
  {
2
2
  "name": "@prontosite/mcp",
3
- "version": "0.10.0",
3
+ "version": "0.10.3",
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": {
7
7
  "prontosite-mcp": "index.mjs"
8
8
  },
9
9
  "main": "index.mjs",
10
+ "scripts": {
11
+ "check": "node --check index.mjs && node --check api.mjs",
12
+ "smoke": "node smoke.mjs",
13
+ "prepublishOnly": "npm run check && npm run smoke"
14
+ },
10
15
  "files": [
11
16
  "index.mjs",
12
17
  "api.mjs",