@prontosite/mcp 0.14.1 → 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,16 @@
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
+
6
16
  ## 0.14.1 – 2026-10-22
7
17
 
8
18
  - 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.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.1",
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
@@ -70,13 +70,37 @@ export function toolAnnotations(name, mode = "local") {
70
70
  return { readOnlyHint: ro, destructiveHint: !ro && DESTRUCTIVE.has(name), idempotentHint: ro || name === "site_lock" || name === "site_unlock", openWorldHint: OPEN_WORLD.has(name) };
71
71
  }
72
72
 
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)) }] });
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
75
91
 
76
92
  /** Alle Tools am MCP-Server registrieren. */
77
93
  export function registerTools(server0, { z, cfg, mode = "local", remote = null }) {
78
94
  // 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) };
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
+ } };
80
104
  const SITE_DESC = mode === "remote"
81
105
  ? "Website: Slug (z. B. 'mas-monti'), Site-ID (s-…), Firmenname, Domain oder Site-Titel (unscharf, z. B. 'Firat-Deveci'). Ohne Angabe "
82
106
  + "die Website des Tokens bzw. der Freigabe. Betreuer: die gewünschte Website angeben (sites_list zeigt alle mit Firmennamen)."
@@ -268,7 +292,7 @@ export function registerTools(server0, { z, cfg, mode = "local", remote = null }
268
292
  const r = await apiMediaSearch(cfg, { site, query, orientation, count });
269
293
  const images = (r.results || []).filter(x => x.preview?.data).map(x => ({ type: "image", data: x.preview.data, mimeType: x.preview.mimeType || "image/jpeg" }));
270
294
  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] };
295
+ return { content: [{ type: "text", text: JSON.stringify(text, null, 2) }, ...images], structuredContent: text };
272
296
  } catch (e) { return asError(e); }
273
297
  }
274
298
  );
@@ -361,9 +385,9 @@ export function registerTools(server0, { z, cfg, mode = "local", remote = null }
361
385
  try {
362
386
  const r = await apiPreviewScreenshot(cfg, { site, page, width });
363
387
  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 }) }] };
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 };
367
391
  } catch (e) { return asError(e); }
368
392
  }
369
393
  );
@@ -585,13 +609,21 @@ export function registerTools(server0, { z, cfg, mode = "local", remote = null }
585
609
  { site: z.string().optional().describe(SITE_DESC) },
586
610
  async ({ site }) => { try { return asText(await apiFiles(cfg, { site })); } catch (e) { return asError(e); } }
587
611
  );
588
- server.tool(
612
+ server.toolWithOutput(
589
613
  "site_file_get",
590
- "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. "
591
619
  + "Der Block overlay nennt Dashboard-Änderungen, die im Entwurf ÜBER diesem Quelltext liegen (sie gewinnen sichtbar) – "
592
620
  + "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); } }
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); } }
595
627
  );
596
628
  server.tool(
597
629
  "site_file_put",