@prontosite/mcp 0.14.1 → 0.14.4

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,30 @@
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.4 – 2026-10-24
7
+
8
+ Enthält 0.14.3 (nie einzeln veröffentlicht).
9
+ - Instructions: „Änderungen sofort live" (Auto-Publish). Meldet `site_status` `autoPublish:true`, nicht nach Veröffentlichung
10
+ fragen und nicht „Entwurf" sagen, sondern „ist live" melden; der Vorschau-Link entfällt, `site_undo` veröffentlicht sofort mit.
11
+ Bestätigung bleibt Pflicht für Domain, Rechtstexte-Einstellungen und das Löschen von Seiten (`autoPublish.state:"held"` →
12
+ nach Bestätigung `site_publish`).
13
+
14
+ ## 0.14.3 – 2026-10-24
15
+
16
+ - Instructions + `site_publish`: Änderungen im Entwurf bündeln und am Ende einmal veröffentlichen, nicht nach jedem Schritt
17
+ (weiterhin nur nach Bestätigung des Nutzers). Neues Publish-Limit des Servers: Kunden 30, Betreuer 120 Veröffentlichungen je
18
+ Stunde, je 10 direkt hintereinander; bei 429 `retryAfter` abwarten, der Entwurf bleibt erhalten.
19
+
20
+ ## 0.14.2 – 2026-10-24
21
+
22
+ - Alle Tools liefern zusätzlich `structuredContent` (dieselben Daten wie der Text-Block, MCP 2025-06-18 „Structured Content");
23
+ Fehler als `structuredContent.error` (außer bei Tools mit `outputSchema` – dort nur Text). Anlass: Befund ChatGPT, `site_file_get` kam als `{}` an.
24
+ - `site_file_get {path, offset?, length?, outline?}`: liest in Teilen (Standard 60 KB, max. 1 MB je Aufruf). Antwort mit
25
+ `size`, `offset`, `length`, `truncated`, `nextOffset`; Einheit Bytes (UTF-8), Teile enden nie mitten in einem Zeichen, Binär-Teile
26
+ (Base64) lassen sich aneinanderhängen. Beim ersten Teil einer großen Datei bzw. mit `outline:true` eine gekappte Gliederung
27
+ (Überschriften, Abschnitte, IDs, CSS-Regeln mit `line` + `offset`) zum gezielten Nachladen. `outputSchema` für `site_file_get`.
28
+ - REST `GET /api/file` ohne die neuen Parameter liefert weiter die ganze Datei (ältere Clients).
29
+
6
30
  ## 0.14.1 – 2026-10-22
7
31
 
8
32
  - Instructions + `media_search`: Fotos nie per URL von Unsplash oder anderen Bilddiensten (images.unsplash.com, imgix …) ins
package/README.md CHANGED
@@ -85,7 +85,7 @@ sichern: `cp ~/.prontosite.json ~/.prontosite.json.bak`).
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
86
  | `site_status({ site? })` | Plan, Slug, Live-URL, Publish-Bedarf, `draftRev` + `recentEdits` (wer hat zuletzt wo geändert). **Lesend.** |
87
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.** |
88
+ | `site_file_get({ site?, path, offset?, length?, outline? })` | Quelltext einer Datei (Text bzw. Base64, Datei max. 5 MB) in Teilen von höchstens `length` Bytes (Standard 60 KB): `truncated` + `nextOffset` zum Weiterlesen, beim ersten Teil bzw. mit `outline:true` eine Gliederung (Abschnitte/IDs mit `line` + `offset`); + `draftRev` + Hinweis auf Dashboard-Änderungen darüber (`overlay`). **Lesend.** |
89
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
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
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. |
package/api.mjs CHANGED
@@ -369,10 +369,15 @@ export async function apiFiles(cfg, { site } = {}, fetchImpl = fetch) {
369
369
  const { token, slug } = resolveTarget(cfg, site);
370
370
  return asJson(await fetchImpl(`${baseUrl(cfg)}/files/${encodeURIComponent(slug)}`, { headers: authHeaders(token, cfg) }));
371
371
  }
372
- /** GET /api/file/<site>?path= – Quelltext einer Datei (Text bzw. Base64) + draftRev + Overlay-Hinweis. */
373
- export async function apiFileGet(cfg, { site, path: rel } = {}, fetchImpl = fetch) {
374
- const { token, slug } = resolveTarget(cfg, site);
375
- return asJson(await fetchImpl(`${baseUrl(cfg)}/file/${encodeURIComponent(slug)}?path=${encodeURIComponent(rel)}`, { headers: authHeaders(token, cfg) }));
372
+ /** GET /api/file/<site>?path=&offset=&length=&outline= – Quelltext einer Datei bzw. ein Teil davon (Text bzw. Base64) + draftRev + Overlay-Hinweis. */
373
+ export async function apiFileGet(cfg, { site, path: rel, offset = null, length = null, outline = false } = {}, fetchImpl = fetch) {
374
+ const { token, slug } = resolveTarget(cfg, site);
375
+ // Teil-Lesen (0.14.2): offset/length in Bytes, outline=1 nur Gliederung – ohne Angaben die ganze Datei (wie bisher).
376
+ const q = new URLSearchParams({ path: rel });
377
+ if (Number.isInteger(offset)) q.set("offset", String(offset));
378
+ if (Number.isInteger(length)) q.set("length", String(length));
379
+ if (outline) q.set("outline", "1");
380
+ return asJson(await fetchImpl(`${baseUrl(cfg)}/file/${encodeURIComponent(slug)}?${q}`, { headers: authHeaders(token, cfg) }));
376
381
  }
377
382
  /** PUT /api/file/<site>?path= – Text-Inhalt ersetzen (Entwurf). baseRev Pflicht. */
378
383
  export async function apiFilePut(cfg, { site, path: rel, content, baseRev, overlay = "keep" } = {}, fetchImpl = fetch) {
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.14.1" }, { instructions: instructionsFor("local") });
24
+ const server = new McpServer({ name: "prontosite", version: "0.14.4" }, { 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.14.1",
3
+ "version": "0.14.4",
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
@@ -35,7 +35,14 @@ const instructionLines = mode => [
35
35
  "2b. Für Übersetzungen und Textüberarbeitung: site_page_texts → Texte übersetzen/überarbeiten → site_page_texts_set, JE SEITE ein Aufruf "
36
36
  + "(neue Sprachfassung vorher mit site_page_copy, z. B. index.html → en/index.html). NIE HTML über site_file_put umschreiben. "
37
37
  + "Rechtstext-Seiten (Impressum/Datenschutz) sind ausgenommen.",
38
- "3. Veröffentlichen (site_publish, publish=true) NUR nach ausdrücklicher Bestätigung des Nutzers im Chat – bis dahin bleibt alles im Entwurf.",
38
+ "3. Veröffentlichen (site_publish, publish=true) NUR nach ausdrücklicher Bestätigung des Nutzers im Chat – bis dahin bleibt alles im Entwurf. "
39
+ + "Änderungen im Entwurf BÜNDELN und am Ende EINMAL veröffentlichen, nicht nach jedem Schritt (Vorschau über previewUrl). Limit: Kunden 30, "
40
+ + "Betreuer 120 Veröffentlichungen je Stunde (je 10 direkt hintereinander); bei 429 retryAfter abwarten, der Entwurf bleibt erhalten.",
41
+ "3a. Wenn site_status autoPublish:true meldet („Änderungen sofort live\"): nicht nach Veröffentlichung fragen und nicht „Entwurf\" sagen – "
42
+ + "„ist live\" melden (geht nach ca. 5 s automatisch live, Folgen von Änderungen werden gebündelt); Vorschau-Link entfällt; site_undo "
43
+ + "veröffentlicht sofort mit. Weiterhin NUR nach Bestätigung des Nutzers: Domain verbinden/trennen, Rechtstexte-Einstellungen "
44
+ + "(Firmendaten, site_legal_options, Impressum/Datenschutz) und Löschen von Seiten. Meldet eine Antwort autoPublish.state \"held\", "
45
+ + "wartet die Änderung auf Bestätigung → nach Bestätigung site_publish.",
39
46
  "4. Konflikt (409, conflict:true): nicht erzwingen. Erst site_status aufrufen (zeigt, wer was geändert hat), Inhalt neu holen, Änderung "
40
47
  + "neu anwenden und mit der neuen draftRev erneut senden.",
41
48
  "5a. Bilder: Für neue Bilder media_upload_begin → Upload-Link. Wenn du Dateien lesen kannst, lade die ORIGINALDATEI selbst per HTTP PUT "
@@ -70,13 +77,37 @@ export function toolAnnotations(name, mode = "local") {
70
77
  return { readOnlyHint: ro, destructiveHint: !ro && DESTRUCTIVE.has(name), idempotentHint: ro || name === "site_lock" || name === "site_unlock", openWorldHint: OPEN_WORLD.has(name) };
71
78
  }
72
79
 
73
- const asText = obj => ({ content: [{ type: "text", text: JSON.stringify(obj, null, 2) }] });
74
- const asError = e => ({ isError: true, content: [{ type: "text", text: "Fehler: " + (e?.message || String(e)) }] });
80
+ // Antworten (MCP 2025-06-18 „Structured Content"): dieselben Daten als structuredContent (JSON-Objekt) UND serialisiert im
81
+ // Text-Block (Rückwärtskompatibilität). ChatGPT wertet structuredContent aus, Claude & Co. den Text (Befund 24.10.2026).
82
+ export const toStructured = obj => (obj && typeof obj === "object" && !Array.isArray(obj) ? obj : Array.isArray(obj) ? { items: obj } : { value: obj ?? null });
83
+ export const asText = obj => ({ content: [{ type: "text", text: JSON.stringify(obj, null, 2) }], structuredContent: toStructured(obj) });
84
+ export const asError = e => { const msg = e?.message || String(e);
85
+ return { isError: true, content: [{ type: "text", text: "Fehler: " + msg }], structuredContent: { error: msg, ...(e?.status ? { status: e.status } : {}) } }; };
86
+
87
+ /** Ausgabe-Schema von site_file_get (structuredContent; OpenAI Apps SDK: outputSchema für Tools mit structuredContent). */
88
+ const FILE_GET_OUTPUT = z => z.object({
89
+ ok: z.boolean().optional(), path: z.string(), type: z.string().optional(), encoding: z.enum(["utf8", "base64"]),
90
+ content: z.string().nullable().describe("Inhalt dieses Teils (utf8 bzw. Base64); null bei outline:true."),
91
+ bytes: z.number().int().optional(), size: z.number().int().describe("Dateigröße in Bytes."),
92
+ offset: z.number().int().describe("Start dieses Teils in Bytes."), length: z.number().int().describe("Länge dieses Teils in Bytes."),
93
+ truncated: z.boolean().describe("true = nicht die ganze Datei."), nextOffset: z.number().int().nullable().describe("Weiterlesen ab hier; null = Ende."),
94
+ outline: z.object({ items: z.array(z.object({ line: z.number().int(), offset: z.number().int() }).passthrough()), total: z.number().int() }).passthrough().optional(),
95
+ hinweis: z.string().optional(), sha: z.string().optional(), source: z.string().optional(), draftRev: z.number().int().optional(),
96
+ page: z.object({ id: z.string(), title: z.string().optional() }).passthrough().optional(), overlay: z.any().optional(),
97
+ }).passthrough(); // weitere Felder erlaubt (additionalProperties) – neue Server-Felder brechen strenge Clients nicht
75
98
 
76
99
  /** Alle Tools am MCP-Server registrieren. */
77
100
  export function registerTools(server0, { z, cfg, mode = "local", remote = null }) {
78
101
  // Jedes Tool mit Annotationen registrieren (Signatur des SDK: name, description, schema, annotations, handler).
79
- const server = { tool: (name, desc, schema, cb) => server0.tool(name, desc, schema, toolAnnotations(name, mode), cb) };
102
+ const server = { tool: (name, desc, schema, cb) => server0.tool(name, desc, schema, toolAnnotations(name, mode), cb),
103
+ // mit Ausgabe-Schema (outputSchema; SDK prüft structuredContent dagegen) – Fallback ohne, falls der Server nur tool() kennt
104
+ // Fehler ohne structuredContent: Clients (z. B. der SDK-Client) prüfen es sonst gegen das Schema und verwerfen die Fehlermeldung.
105
+ toolWithOutput: (name, desc, schema, outputSchema, cb) => {
106
+ const wrapped = async (...args) => { const r = await cb(...args); if (r?.isError) { const { structuredContent, ...rest } = r; return rest; } return r; };
107
+ return typeof server0.registerTool === "function"
108
+ ? server0.registerTool(name, { description: desc, inputSchema: schema, outputSchema, annotations: toolAnnotations(name, mode) }, wrapped)
109
+ : server0.tool(name, desc, schema, toolAnnotations(name, mode), wrapped);
110
+ } };
80
111
  const SITE_DESC = mode === "remote"
81
112
  ? "Website: Slug (z. B. 'mas-monti'), Site-ID (s-…), Firmenname, Domain oder Site-Titel (unscharf, z. B. 'Firat-Deveci'). Ohne Angabe "
82
113
  + "die Website des Tokens bzw. der Freigabe. Betreuer: die gewünschte Website angeben (sites_list zeigt alle mit Firmennamen)."
@@ -268,7 +299,7 @@ export function registerTools(server0, { z, cfg, mode = "local", remote = null }
268
299
  const r = await apiMediaSearch(cfg, { site, query, orientation, count });
269
300
  const images = (r.results || []).filter(x => x.preview?.data).map(x => ({ type: "image", data: x.preview.data, mimeType: x.preview.mimeType || "image/jpeg" }));
270
301
  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] };
302
+ return { content: [{ type: "text", text: JSON.stringify(text, null, 2) }, ...images], structuredContent: text };
272
303
  } catch (e) { return asError(e); }
273
304
  }
274
305
  );
@@ -361,9 +392,9 @@ export function registerTools(server0, { z, cfg, mode = "local", remote = null }
361
392
  try {
362
393
  const r = await apiPreviewScreenshot(cfg, { site, page, width });
363
394
  if (!r.png) return asText(r);
364
- return { content: [{ type: "image", data: r.png, mimeType: "image/png" },
365
- { type: "text", text: JSON.stringify({ page: r.page, width: r.width, height: r.height, ...(r.truncated ? { abgeschnitten: `Seite ist ${r.fullHeight} px hoch – gezeigt die oberen ${r.height} px` } : {}),
366
- ...(r.overflowX ? { horizontalerUeberlauf: `Inhalt ist ${r.overflowX} px breit (Viewport ${r.width}) – die Seite scrollt seitwärts; meist ein Bild/Element ohne max-width.` } : {}), ms: r.ms }) }] };
395
+ const info = { page: r.page, width: r.width, height: r.height, ...(r.truncated ? { abgeschnitten: `Seite ist ${r.fullHeight} px hoch – gezeigt die oberen ${r.height} px` } : {}),
396
+ ...(r.overflowX ? { horizontalerUeberlauf: `Inhalt ist ${r.overflowX} px breit (Viewport ${r.width}) – die Seite scrollt seitwärts; meist ein Bild/Element ohne max-width.` } : {}), ms: r.ms };
397
+ return { content: [{ type: "image", data: r.png, mimeType: "image/png" }, { type: "text", text: JSON.stringify(info) }], structuredContent: info };
367
398
  } catch (e) { return asError(e); }
368
399
  }
369
400
  );
@@ -431,7 +462,8 @@ export function registerTools(server0, { z, cfg, mode = "local", remote = null }
431
462
  server.tool(
432
463
  "site_publish",
433
464
  "SCHREIBEND – LIVE FÜR BESUCHER: Veröffentlicht den aktuellen Entwurf einer Website. NUR nach ausdrücklicher Bestätigung "
434
- + "des Nutzers im Chat aufrufen (vorher kurz zusammenfassen, was live geht, und die richtige Website prüfen). Meldet die Antwort "
465
+ + "des Nutzers im Chat aufrufen (vorher kurz zusammenfassen, was live geht, und die richtige Website prüfen). Erst alle Änderungen "
466
+ + "im Entwurf erledigen, dann EINMAL veröffentlichen – nicht nach jedem Schritt (Limit je Stunde, bei 429 retryAfter abwarten). Meldet die Antwort "
435
467
  + "nothing:true (keine erkannte Änderung), aber du willst trotzdem neu ausliefern, setze force:true.",
436
468
  { site: z.string().optional().describe(SITE_DESC),
437
469
  force: z.boolean().default(false).describe("true = trotzdem veröffentlichen, auch ohne erkannte Änderung (wie der Menüpunkt Erneut veröffentlichen).") },
@@ -585,13 +617,21 @@ export function registerTools(server0, { z, cfg, mode = "local", remote = null }
585
617
  { site: z.string().optional().describe(SITE_DESC) },
586
618
  async ({ site }) => { try { return asText(await apiFiles(cfg, { site })); } catch (e) { return asError(e); } }
587
619
  );
588
- server.tool(
620
+ server.toolWithOutput(
589
621
  "site_file_get",
590
- "LESEND: Liefert den Quelltext einer Datei des Entwurfs (Text; Binär als Base64, max. 5 MB) samt draftRev. "
622
+ "LESEND: Liefert den Quelltext einer Datei des Entwurfs (Text; Binär als Base64, Datei max. 5 MB) samt draftRev – in Teilen von "
623
+ + "höchstens length Bytes (Standard 60 KB). truncated:true + nextOffset → mit offset:nextOffset weiterlesen; beim ersten Teil einer "
624
+ + "großen Datei kommt eine Gliederung (outline: Überschriften/Abschnitte/IDs mit line + offset), outline:true liefert NUR die "
625
+ + "Gliederung – damit gezielt nachladen. offset/length/nextOffset sind Bytes (UTF-8), Teile enden nie mitten in einem Zeichen. "
626
+ + "site_file_put ersetzt die GANZE Datei: nie mit einem Teil aufrufen – für gezielte Änderungen site_edit_text/site_edit_html. "
591
627
  + "Der Block overlay nennt Dashboard-Änderungen, die im Entwurf ÜBER diesem Quelltext liegen (sie gewinnen sichtbar) – "
592
628
  + "berücksichtige sie, bevor du Text änderst. Merke dir draftRev: du brauchst sie als baseRev für site_file_put. " + FILE_SMALL,
593
- { site: z.string().optional().describe(SITE_DESC), path: z.string().describe("Relativer Pfad, z. B. 'index.html' oder 'css/style.css' (aus site_files).") },
594
- async ({ site, path }) => { try { return asText(await apiFileGet(cfg, { site, path })); } catch (e) { return asError(e); } }
629
+ { site: z.string().optional().describe(SITE_DESC), path: z.string().describe("Relativer Pfad, z. B. 'index.html' oder 'css/style.css' (aus site_files)."),
630
+ offset: z.number().int().min(0).default(0).describe("Start in Bytes (aus nextOffset bzw. outline[].offset)."),
631
+ length: z.number().int().min(1).max(1048576).default(61440).describe("Höchstens so viele Bytes (Standard 60 KB, max. 1 MB)."),
632
+ outline: z.boolean().optional().describe("true = nur die Gliederung (ohne Inhalt) – für große Dateien zuerst.") },
633
+ FILE_GET_OUTPUT(z),
634
+ async ({ site, path, offset, length, outline }) => { try { return asText(await apiFileGet(cfg, { site, path, offset, length, outline })); } catch (e) { return asError(e); } }
595
635
  );
596
636
  server.tool(
597
637
  "site_file_put",