@prontosite/mcp 0.14.0 → 0.14.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,22 @@
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.2 – 2026-10-24
7
+
8
+ - Alle Tools liefern zusätzlich `structuredContent` (dieselben Daten wie der Text-Block, MCP 2025-06-18 „Structured Content");
9
+ Fehler als `structuredContent.error` (außer bei Tools mit `outputSchema` – dort nur Text). Anlass: Befund ChatGPT, `site_file_get` kam als `{}` an.
10
+ - `site_file_get {path, offset?, length?, outline?}`: liest in Teilen (Standard 60 KB, max. 1 MB je Aufruf). Antwort mit
11
+ `size`, `offset`, `length`, `truncated`, `nextOffset`; Einheit Bytes (UTF-8), Teile enden nie mitten in einem Zeichen, Binär-Teile
12
+ (Base64) lassen sich aneinanderhängen. Beim ersten Teil einer großen Datei bzw. mit `outline:true` eine gekappte Gliederung
13
+ (Überschriften, Abschnitte, IDs, CSS-Regeln mit `line` + `offset`) zum gezielten Nachladen. `outputSchema` für `site_file_get`.
14
+ - REST `GET /api/file` ohne die neuen Parameter liefert weiter die ganze Datei (ältere Clients).
15
+
16
+ ## 0.14.1 – 2026-10-22
17
+
18
+ - Instructions + `media_search`: Fotos nie per URL von Unsplash oder anderen Bilddiensten (images.unsplash.com, imgix …) ins
19
+ HTML/CSS schreiben – sonst lädt jeder Besucher das Bild beim Dienst (Besucher-IPs in die USA, Einwilligungspflicht); immer
20
+ `media_import`, ProntoSite liefert das Foto selbst aus.
21
+
6
22
  ## 0.14.0 – 2026-10-22
7
23
 
8
24
  Enthält 0.13.8 (nie einzeln veröffentlicht).
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.0" }, { instructions: instructionsFor("local") });
24
+ const server = new McpServer({ name: "prontosite", version: "0.14.2" }, { 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.0",
3
+ "version": "0.14.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": {
package/tools.mjs CHANGED
@@ -44,7 +44,8 @@ const instructionLines = mode => [
44
44
  ...(mode === "local" ? ["5a-lokal. Wenn du einen lokalen Dateipfad hast, nutze media_upload_file; sonst Upload-Link (media_upload_begin). "
45
45
  + "media_upload_file prüft, lädt hoch und übernimmt in einem Schritt und liefert die mediaId."] : []),
46
46
  "5e. Bei „passendes Bild für Abschnitt X\": Abschnittstext lesen (site_page_texts), Suchbegriff bilden, media_search, dem Nutzer 2–3 "
47
- + "Vorschauen zeigen und wählen lassen, dann media_import + site_image_set + site_preview_screenshot.",
47
+ + "Vorschauen zeigen und wählen lassen, dann media_import + site_image_set + site_preview_screenshot. Fotos NIE per URL von Unsplash "
48
+ + "oder anderen Bilddiensten (images.unsplash.com, imgix …) ins HTML/CSS schreiben – sonst lädt jeder Besucher beim Dienst; immer media_import.",
48
49
  "5b. Design = CSS-Datei ändern (site_file_get/site_file_put), nicht HTML umschreiben. Nach Design- oder Bild-Änderungen "
49
50
  + "site_preview_screenshot (1280 bzw. 390) prüfen, BEVOR du dem Nutzer antwortest.",
50
51
  "5c. Bei „rückgängig“ site_undo (bzw. „doch wieder“ site_redo), nie Dateien manuell zurückbauen. Medien löschen nur mit site_media_delete "
@@ -69,13 +70,37 @@ export function toolAnnotations(name, mode = "local") {
69
70
  return { readOnlyHint: ro, destructiveHint: !ro && DESTRUCTIVE.has(name), idempotentHint: ro || name === "site_lock" || name === "site_unlock", openWorldHint: OPEN_WORLD.has(name) };
70
71
  }
71
72
 
72
- const asText = obj => ({ content: [{ type: "text", text: JSON.stringify(obj, null, 2) }] });
73
- const asError = e => ({ isError: true, content: [{ type: "text", text: "Fehler: " + (e?.message || String(e)) }] });
73
+ // Antworten (MCP 2025-06-18 „Structured Content"): dieselben Daten als structuredContent (JSON-Objekt) UND serialisiert im
74
+ // Text-Block (Rückwärtskompatibilität). ChatGPT wertet structuredContent aus, Claude & Co. den Text (Befund 24.10.2026).
75
+ export const toStructured = obj => (obj && typeof obj === "object" && !Array.isArray(obj) ? obj : Array.isArray(obj) ? { items: obj } : { value: obj ?? null });
76
+ export const asText = obj => ({ content: [{ type: "text", text: JSON.stringify(obj, null, 2) }], structuredContent: toStructured(obj) });
77
+ export const asError = e => { const msg = e?.message || String(e);
78
+ return { isError: true, content: [{ type: "text", text: "Fehler: " + msg }], structuredContent: { error: msg, ...(e?.status ? { status: e.status } : {}) } }; };
79
+
80
+ /** Ausgabe-Schema von site_file_get (structuredContent; OpenAI Apps SDK: outputSchema für Tools mit structuredContent). */
81
+ const FILE_GET_OUTPUT = z => z.object({
82
+ ok: z.boolean().optional(), path: z.string(), type: z.string().optional(), encoding: z.enum(["utf8", "base64"]),
83
+ content: z.string().nullable().describe("Inhalt dieses Teils (utf8 bzw. Base64); null bei outline:true."),
84
+ bytes: z.number().int().optional(), size: z.number().int().describe("Dateigröße in Bytes."),
85
+ offset: z.number().int().describe("Start dieses Teils in Bytes."), length: z.number().int().describe("Länge dieses Teils in Bytes."),
86
+ truncated: z.boolean().describe("true = nicht die ganze Datei."), nextOffset: z.number().int().nullable().describe("Weiterlesen ab hier; null = Ende."),
87
+ outline: z.object({ items: z.array(z.object({ line: z.number().int(), offset: z.number().int() }).passthrough()), total: z.number().int() }).passthrough().optional(),
88
+ hinweis: z.string().optional(), sha: z.string().optional(), source: z.string().optional(), draftRev: z.number().int().optional(),
89
+ page: z.object({ id: z.string(), title: z.string().optional() }).passthrough().optional(), overlay: z.any().optional(),
90
+ }).passthrough(); // weitere Felder erlaubt (additionalProperties) – neue Server-Felder brechen strenge Clients nicht
74
91
 
75
92
  /** Alle Tools am MCP-Server registrieren. */
76
93
  export function registerTools(server0, { z, cfg, mode = "local", remote = null }) {
77
94
  // Jedes Tool mit Annotationen registrieren (Signatur des SDK: name, description, schema, annotations, handler).
78
- const server = { tool: (name, desc, schema, cb) => server0.tool(name, desc, schema, toolAnnotations(name, mode), cb) };
95
+ const server = { tool: (name, desc, schema, cb) => server0.tool(name, desc, schema, toolAnnotations(name, mode), cb),
96
+ // mit Ausgabe-Schema (outputSchema; SDK prüft structuredContent dagegen) – Fallback ohne, falls der Server nur tool() kennt
97
+ // Fehler ohne structuredContent: Clients (z. B. der SDK-Client) prüfen es sonst gegen das Schema und verwerfen die Fehlermeldung.
98
+ toolWithOutput: (name, desc, schema, outputSchema, cb) => {
99
+ const wrapped = async (...args) => { const r = await cb(...args); if (r?.isError) { const { structuredContent, ...rest } = r; return rest; } return r; };
100
+ return typeof server0.registerTool === "function"
101
+ ? server0.registerTool(name, { description: desc, inputSchema: schema, outputSchema, annotations: toolAnnotations(name, mode) }, wrapped)
102
+ : server0.tool(name, desc, schema, toolAnnotations(name, mode), wrapped);
103
+ } };
79
104
  const SITE_DESC = mode === "remote"
80
105
  ? "Website: Slug (z. B. 'mas-monti'), Site-ID (s-…), Firmenname, Domain oder Site-Titel (unscharf, z. B. 'Firat-Deveci'). Ohne Angabe "
81
106
  + "die Website des Tokens bzw. der Freigabe. Betreuer: die gewünschte Website angeben (sites_list zeigt alle mit Firmennamen)."
@@ -257,7 +282,8 @@ export function registerTools(server0, { z, cfg, mode = "local", remote = null }
257
282
  "media_search",
258
283
  "Kostenlose Fotos von Unsplash suchen (verbraucht ein gemeinsames Stunden-Kontingent; gleiche Suchen kommen 1 h aus dem Cache). "
259
284
  + "Liefert je Treffer eine kleine Vorschau als Bild, Fotograf, Lizenz-Hinweis und eine resultId (1 h gültig). Suchbegriff am besten "
260
- + "englisch und konkret (z. B. 'bakery counter bread'). Dem Nutzer 2–3 Vorschauen zeigen und wählen lassen, dann media_import.",
285
+ + "englisch und konkret (z. B. 'bakery counter bread'). Dem Nutzer 2–3 Vorschauen zeigen und wählen lassen, dann media_import "
286
+ + "(nie die Unsplash-URL direkt einbinden).",
261
287
  { site: z.string().optional().describe(SITE_DESC), query: z.string().describe("Suchbegriff (2–120 Zeichen)."),
262
288
  orientation: z.enum(["landscape", "portrait", "squarish"]).optional().describe("Ausrichtung: landscape (Hero/Banner), portrait, squarish."),
263
289
  count: z.number().int().min(1).max(6).default(3).describe("Anzahl Treffer (max. 6).") },
@@ -266,7 +292,7 @@ export function registerTools(server0, { z, cfg, mode = "local", remote = null }
266
292
  const r = await apiMediaSearch(cfg, { site, query, orientation, count });
267
293
  const images = (r.results || []).filter(x => x.preview?.data).map(x => ({ type: "image", data: x.preview.data, mimeType: x.preview.mimeType || "image/jpeg" }));
268
294
  const text = { ...r, results: (r.results || []).map((x, i) => ({ nr: i + 1, ...x, preview: x.preview ? "(Bild unten)" : null })) };
269
- return { content: [{ type: "text", text: JSON.stringify(text, null, 2) }, ...images] };
295
+ return { content: [{ type: "text", text: JSON.stringify(text, null, 2) }, ...images], structuredContent: text };
270
296
  } catch (e) { return asError(e); }
271
297
  }
272
298
  );
@@ -359,9 +385,9 @@ export function registerTools(server0, { z, cfg, mode = "local", remote = null }
359
385
  try {
360
386
  const r = await apiPreviewScreenshot(cfg, { site, page, width });
361
387
  if (!r.png) return asText(r);
362
- return { content: [{ type: "image", data: r.png, mimeType: "image/png" },
363
- { 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` } : {}),
364
- ...(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 }) }] };
388
+ 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` } : {}),
389
+ ...(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 };
390
+ return { content: [{ type: "image", data: r.png, mimeType: "image/png" }, { type: "text", text: JSON.stringify(info) }], structuredContent: info };
365
391
  } catch (e) { return asError(e); }
366
392
  }
367
393
  );
@@ -583,13 +609,21 @@ export function registerTools(server0, { z, cfg, mode = "local", remote = null }
583
609
  { site: z.string().optional().describe(SITE_DESC) },
584
610
  async ({ site }) => { try { return asText(await apiFiles(cfg, { site })); } catch (e) { return asError(e); } }
585
611
  );
586
- server.tool(
612
+ server.toolWithOutput(
587
613
  "site_file_get",
588
- "LESEND: Liefert den Quelltext einer Datei des Entwurfs (Text; Binär als Base64, max. 5 MB) samt draftRev. "
614
+ "LESEND: Liefert den Quelltext einer Datei des Entwurfs (Text; Binär als Base64, Datei max. 5 MB) samt draftRev – in Teilen von "
615
+ + "höchstens length Bytes (Standard 60 KB). truncated:true + nextOffset → mit offset:nextOffset weiterlesen; beim ersten Teil einer "
616
+ + "großen Datei kommt eine Gliederung (outline: Überschriften/Abschnitte/IDs mit line + offset), outline:true liefert NUR die "
617
+ + "Gliederung – damit gezielt nachladen. offset/length/nextOffset sind Bytes (UTF-8), Teile enden nie mitten in einem Zeichen. "
618
+ + "site_file_put ersetzt die GANZE Datei: nie mit einem Teil aufrufen – für gezielte Änderungen site_edit_text/site_edit_html. "
589
619
  + "Der Block overlay nennt Dashboard-Änderungen, die im Entwurf ÜBER diesem Quelltext liegen (sie gewinnen sichtbar) – "
590
620
  + "berücksichtige sie, bevor du Text änderst. Merke dir draftRev: du brauchst sie als baseRev für site_file_put. " + FILE_SMALL,
591
- { site: z.string().optional().describe(SITE_DESC), path: z.string().describe("Relativer Pfad, z. B. 'index.html' oder 'css/style.css' (aus site_files).") },
592
- async ({ site, path }) => { try { return asText(await apiFileGet(cfg, { site, path })); } catch (e) { return asError(e); } }
621
+ { site: z.string().optional().describe(SITE_DESC), path: z.string().describe("Relativer Pfad, z. B. 'index.html' oder 'css/style.css' (aus site_files)."),
622
+ offset: z.number().int().min(0).default(0).describe("Start in Bytes (aus nextOffset bzw. outline[].offset)."),
623
+ length: z.number().int().min(1).max(1048576).default(61440).describe("Höchstens so viele Bytes (Standard 60 KB, max. 1 MB)."),
624
+ outline: z.boolean().optional().describe("true = nur die Gliederung (ohne Inhalt) – für große Dateien zuerst.") },
625
+ FILE_GET_OUTPUT(z),
626
+ async ({ site, path, offset, length, outline }) => { try { return asText(await apiFileGet(cfg, { site, path, offset, length, outline })); } catch (e) { return asError(e); } }
593
627
  );
594
628
  server.tool(
595
629
  "site_file_put",