@prontosite/mcp 0.13.7 → 0.14.0
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 +21 -0
- package/api.mjs +23 -2
- package/index.mjs +1 -1
- package/package.json +1 -1
- package/tools.mjs +49 -3
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,27 @@
|
|
|
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.0 – 2026-10-22
|
|
7
|
+
|
|
8
|
+
Enthält 0.13.8 (nie einzeln veröffentlicht).
|
|
9
|
+
- Neu: `media_search {site, query, orientation?, count≤6}` – Unsplash-Fotos suchen; je Treffer eine kleine Vorschau als Bild,
|
|
10
|
+
Fotograf, Lizenz-Hinweis und `resultId` (1 h gültig). Gleiche Suchen kommen 1 h aus dem Cache.
|
|
11
|
+
- Neu: `media_import {site, resultId}` – Treffer übernehmen (Download-Trigger laut Unsplash-Richtlinie, Varianten, Kontingent,
|
|
12
|
+
Fotograf/Quelle in den Medien-Metadaten) → `mediaId` + vorbelegter alt-Text; danach `site_image_set`.
|
|
13
|
+
- Instructions: „Bei ‚passendes Bild für Abschnitt X': site_page_texts → media_search → 2–3 Vorschauen zur Wahl → media_import
|
|
14
|
+
+ site_image_set + Screenshot."
|
|
15
|
+
|
|
16
|
+
## 0.13.8 – 2026-10-21
|
|
17
|
+
|
|
18
|
+
- Neu: `site_edit_html {find, replace, region?, scope?, pages?}` – Markup-Fragment (z. B. Footer) über alle Seiten in EINEM
|
|
19
|
+
Aufruf ersetzen, ein Rückgängig-Schritt; mehrere Fundstellen ohne `scope:"all"` → nur Liste. Rechtstext-Bereich, Skript-Regeln
|
|
20
|
+
und das ProntoSite-Badge geschützt (`skipped`).
|
|
21
|
+
- Badge-Sperre: `site_edit_text`, `site_edit_html` und `site_file_put` ändern, entfernen oder bauen das Badge (`data-ps="badge"`)
|
|
22
|
+
nicht nach – es wird beim Ausliefern erzeugt; Änderungen daneben und das Wiederherstellen des Original-Markers bleiben erlaubt.
|
|
23
|
+
- Rate-Limit (429): Meldung nennt die Rest-Wartezeit und den richtigen Weg (statt allgemeinem Text). Schreib-Limit je Token/Grant
|
|
24
|
+
und Konto: Betreuer 200, Kunden 30 Aufrufe je 10 Minuten.
|
|
25
|
+
- Instructions: Markup über alle Seiten per `site_edit_html`, nie Seite für Seite per `site_file_put`; Badge nie im HTML ändern.
|
|
26
|
+
|
|
6
27
|
## 0.13.7 – 2026-10-20
|
|
7
28
|
|
|
8
29
|
- Neu: `site_forms_get` – Konto-Empfänger, je Formular Art (verwaltetes ProntoSite-Formular oder eigenes `<form>` aus dem
|
package/api.mjs
CHANGED
|
@@ -115,8 +115,8 @@ export function friendlyApiError(status, serverError = "") {
|
|
|
115
115
|
return "Dein API-Token ist ungültig oder abgelaufen. Token im Dashboard → API-Zugang neu erstellen und in ~/.prontosite.json eintragen.";
|
|
116
116
|
if (status === 403 && /API-Zugang nicht im Tarif|Starter-Plan/i.test(raw))
|
|
117
117
|
return "Dein ProntoSite-Tarif enthält keinen API-Zugang. Wähle im Dashboard → Zahlung einen Tarif oder bitte deinen Betreuer um eine API-Freigabe.";
|
|
118
|
-
if (status === 429)
|
|
119
|
-
return "Zu viele Anfragen in kurzer Zeit (Rate-Limit). Bitte einen Moment warten und erneut versuchen.";
|
|
118
|
+
if (status === 429) // Server nennt Wartezeit + richtigen Weg → durchreichen; sonst allgemeiner Hinweis
|
|
119
|
+
return /wieder möglich in/.test(raw) ? raw : "Zu viele Anfragen in kurzer Zeit (Rate-Limit). Bitte einen Moment warten und erneut versuchen.";
|
|
120
120
|
return raw || `HTTP ${status}`;
|
|
121
121
|
}
|
|
122
122
|
|
|
@@ -402,6 +402,15 @@ export async function apiEditText(cfg, { site, find, replace, scope = null, regi
|
|
|
402
402
|
method: "POST", headers: { ...authHeaders(token, cfg), "content-type": "application/json" }, body: JSON.stringify(body) }));
|
|
403
403
|
}
|
|
404
404
|
|
|
405
|
+
/** POST /api/edit-html/<site> – Markup-Fragment über alle Seiten ersetzen (z. B. Footer), EIN Undo-Eintrag. 409 als Objekt. */
|
|
406
|
+
export async function apiEditHtml(cfg, { site, find, replace, scope = null, region = null, pages = null, baseRev = null } = {}, fetchImpl = fetch) {
|
|
407
|
+
const { token, slug } = resolveTarget(cfg, site);
|
|
408
|
+
const body = { find, replace, ...(scope ? { scope } : {}), ...(region ? { region } : {}),
|
|
409
|
+
...(Array.isArray(pages) && pages.length ? { pages } : {}), ...(Number.isInteger(baseRev) ? { baseRev } : {}) };
|
|
410
|
+
return asJsonOr409(await fetchImpl(`${baseUrl(cfg)}/edit-html/${encodeURIComponent(slug)}`, {
|
|
411
|
+
method: "POST", headers: { ...authHeaders(token, cfg), "content-type": "application/json" }, body: JSON.stringify(body) }));
|
|
412
|
+
}
|
|
413
|
+
|
|
405
414
|
/** GET /api/page-texts/<site>?page= – alle Texte einer Seite (Textknoten, reine Text-Links, Attribute, Titel/Beschreibung). */
|
|
406
415
|
export async function apiPageTexts(cfg, { site, page } = {}, fetchImpl = fetch) {
|
|
407
416
|
const { token, slug } = resolveTarget(cfg, site);
|
|
@@ -551,6 +560,18 @@ export async function apiMediaRestore(cfg, { site, trashId } = {}, fetchImpl = f
|
|
|
551
560
|
return asJson(await fetchImpl(`${baseUrl(cfg)}/media-restore/${encodeURIComponent(slug)}`, {
|
|
552
561
|
method: "POST", headers: { ...authHeaders(token, cfg), "content-type": "application/json" }, body: JSON.stringify({ trashId }) }));
|
|
553
562
|
}
|
|
563
|
+
/** GET /api/media-search/<site>?query=&orientation=&count= – Unsplash-Treffer (Vorschau base64, Fotograf, resultId). */
|
|
564
|
+
export async function apiMediaSearch(cfg, { site, query, orientation = null, count = 3 } = {}, fetchImpl = fetch) {
|
|
565
|
+
const { token, slug } = resolveTarget(cfg, site);
|
|
566
|
+
const q = new URLSearchParams({ query: String(query ?? ""), count: String(count ?? 3), ...(orientation ? { orientation } : {}) });
|
|
567
|
+
return asJson(await fetchImpl(`${baseUrl(cfg)}/media-search/${encodeURIComponent(slug)}?${q}`, { headers: authHeaders(token, cfg) }));
|
|
568
|
+
}
|
|
569
|
+
/** POST /api/media-import/<site> – Unsplash-Treffer übernehmen (Download-Trigger, Varianten, Attribution) → mediaId. */
|
|
570
|
+
export async function apiMediaImport(cfg, { site, resultId } = {}, fetchImpl = fetch) {
|
|
571
|
+
const { token, slug } = resolveTarget(cfg, site);
|
|
572
|
+
return asJson(await fetchImpl(`${baseUrl(cfg)}/media-import/${encodeURIComponent(slug)}`, {
|
|
573
|
+
method: "POST", headers: { ...authHeaders(token, cfg), "content-type": "application/json" }, body: JSON.stringify({ resultId }) }));
|
|
574
|
+
}
|
|
554
575
|
/** GET /api/preview-screenshot/<site>?page=&width= – PNG des Entwurfs (Base64 + Maße). */
|
|
555
576
|
export async function apiPreviewScreenshot(cfg, { site, page = "index.html", width = 1280 } = {}, fetchImpl = fetch) {
|
|
556
577
|
const { token, slug } = resolveTarget(cfg, site);
|
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.
|
|
24
|
+
const server = new McpServer({ name: "prontosite", version: "0.14.0" }, { 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.
|
|
3
|
+
"version": "0.14.0",
|
|
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
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* site_domain_connect nur mit Betreuer-Token (Entscheidung 9a).
|
|
10
10
|
* `z` wird vom Aufrufer übergeben (dieselbe zod-Instanz wie sein MCP-SDK).
|
|
11
11
|
*/
|
|
12
|
-
import { apiEditText, apiPageTexts, apiPageTextsSet, apiPageCopy, apiMediaUploadBegin, apiMediaUploadFile, apiMediaCommit, apiMediaList, apiImageSet, apiUndo, apiMediaDelete, apiMediaRestore, apiFormsGet, apiFormsSet, apiPreviewScreenshot, apiStatus, apiExport, apiExportFetch, apiDeploy, apiPublish, sitesList, apiAccountCreate, apiCompanyGet, apiCompanySet, apiLegalOptions, apiDomainStatus, apiDomainConnect, apiEmbeds, apiEmbedConsent, apiWidgets, apiWidgetSet, apiFiles, apiFileGet, apiFilePut, apiFileDelete, apiFileRename, apiLock } from "./api.mjs";
|
|
12
|
+
import { apiEditText, apiEditHtml, apiPageTexts, apiPageTextsSet, apiPageCopy, apiMediaUploadBegin, apiMediaUploadFile, apiMediaCommit, apiMediaList, apiImageSet, apiUndo, apiMediaDelete, apiMediaRestore, apiFormsGet, apiFormsSet, apiMediaSearch, apiMediaImport, apiPreviewScreenshot, apiStatus, apiExport, apiExportFetch, apiDeploy, apiPublish, sitesList, apiAccountCreate, apiCompanyGet, apiCompanySet, apiLegalOptions, apiDomainStatus, apiDomainConnect, apiEmbeds, apiEmbedConsent, apiWidgets, apiWidgetSet, apiFiles, apiFileGet, apiFilePut, apiFileDelete, apiFileRename, apiLock } from "./api.mjs";
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* Arbeitsweise (MCP-`instructions` beim Initialize – lokal UND /mcp): Weg B ist der Standard (Nick 19.10.).
|
|
@@ -29,6 +29,9 @@ const instructionLines = mode => [
|
|
|
29
29
|
"2a. Für Text-, Link- und Kontaktdaten-Änderungen (Telefonnummer, E-Mail, Adresse, Öffnungszeiten) IMMER site_edit_text mit "
|
|
30
30
|
+ "scope:\"all\" (bei Bedarf region:\"footer\"/\"header\") – EIN Aufruf für alle Seiten, inkl. tel:/mailto:-Links. NIE Seite für Seite per "
|
|
31
31
|
+ "site_file_put. Meldet die Antwort company, zusätzlich site_company_set für die Firmendaten (Impressum/Datenschutz/Kontakt).",
|
|
32
|
+
"2c. Für Markup über alle Seiten (z. B. Footer-HTML) site_edit_html mit scope:\"all\" (ggf. region:\"footer\"), nie Seite für Seite per site_file_put. "
|
|
33
|
+
+ "Schreib-Limit je 10 Minuten: Kunden 30, Betreuer 200 Aufrufe – dieselbe Änderung auf vielen Seiten ist EIN Aufruf (site_edit_text scope:\"all\" bzw. site_edit_html).",
|
|
34
|
+
"2d. Badge nie im HTML ändern, nachbauen oder entfernen – es wird beim Ausliefern erzeugt (data-ps=\"badge\"; site_edit_text/site_edit_html/site_file_put lehnen das ab, erlaubt ist nur das Wiederherstellen des Original-Markers).",
|
|
32
35
|
"2b. Für Übersetzungen und Textüberarbeitung: site_page_texts → Texte übersetzen/überarbeiten → site_page_texts_set, JE SEITE ein Aufruf "
|
|
33
36
|
+ "(neue Sprachfassung vorher mit site_page_copy, z. B. index.html → en/index.html). NIE HTML über site_file_put umschreiben. "
|
|
34
37
|
+ "Rechtstext-Seiten (Impressum/Datenschutz) sind ausgenommen.",
|
|
@@ -40,6 +43,8 @@ const instructionLines = mode => [
|
|
|
40
43
|
+ "media_upload_commit und mit site_image_set einsetzen. Nie Varianten selbst bauen, nie ZIP für Medien.",
|
|
41
44
|
...(mode === "local" ? ["5a-lokal. Wenn du einen lokalen Dateipfad hast, nutze media_upload_file; sonst Upload-Link (media_upload_begin). "
|
|
42
45
|
+ "media_upload_file prüft, lädt hoch und übernimmt in einem Schritt und liefert die mediaId."] : []),
|
|
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.",
|
|
43
48
|
"5b. Design = CSS-Datei ändern (site_file_get/site_file_put), nicht HTML umschreiben. Nach Design- oder Bild-Änderungen "
|
|
44
49
|
+ "site_preview_screenshot (1280 bzw. 390) prüfen, BEVOR du dem Nutzer antwortest.",
|
|
45
50
|
"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 "
|
|
@@ -54,10 +59,10 @@ export const SERVER_INSTRUCTIONS = instructionsFor("remote");
|
|
|
54
59
|
// Tool-Annotationen (MCP ToolAnnotations): ChatGPT & Co. fragen sonst bei JEDEM Aufruf nach. readOnly = ändert nichts;
|
|
55
60
|
// destructive = live/kundenwirksam, ersetzt Bestand oder nimmt zurück (Bestätigung sinnvoll). Entwurfs-Änderungen sind
|
|
56
61
|
// rückgängig machbar und daher nicht destruktiv. openWorld = wirkt nach außen (öffentliche Website/DNS).
|
|
57
|
-
const READ_ONLY = new Set(["sites_list", "site_status", "site_forms_get", "site_page_texts", "site_media_list", "site_preview_screenshot", "site_company_get",
|
|
62
|
+
const READ_ONLY = new Set(["sites_list", "site_status", "site_forms_get", "media_search", "site_page_texts", "site_media_list", "site_preview_screenshot", "site_company_get",
|
|
58
63
|
"site_domain_status", "site_embeds", "site_widgets", "site_files", "site_file_get"]);
|
|
59
64
|
const DESTRUCTIVE = new Set(["site_publish", "site_deploy", "deploy_commit", "site_file_delete", "site_media_delete", "site_undo", "site_redo", "site_domain_connect"]);
|
|
60
|
-
const OPEN_WORLD = new Set(["site_publish", "site_deploy", "deploy_commit", "site_domain_connect"]);
|
|
65
|
+
const OPEN_WORLD = new Set(["site_publish", "site_deploy", "deploy_commit", "site_domain_connect", "media_search", "media_import"]);
|
|
61
66
|
export function toolAnnotations(name, mode = "local") {
|
|
62
67
|
// site_export: remote nur ein Download-Link (lesend), lokal schreibt es eine ZIP auf die Platte.
|
|
63
68
|
const ro = READ_ONLY.has(name) || (name === "site_export" && mode === "remote");
|
|
@@ -165,6 +170,22 @@ export function registerTools(server0, { z, cfg, mode = "local", remote = null }
|
|
|
165
170
|
async ({ site, find, replace, scope, region, attributes, pages, baseRev }) => { try { return asText(await apiEditText(cfg, { site, find, replace, scope, region, attributes, pages, baseRev })); } catch (e) { return asError(e); } }
|
|
166
171
|
);
|
|
167
172
|
|
|
173
|
+
server.tool(
|
|
174
|
+
"site_edit_html",
|
|
175
|
+
"SCHREIBEND (Entwurf, nicht live) – Markup-Fragment über ALLE Seiten in EINEM Aufruf ersetzen (z. B. Footer-HTML: neuer Link, "
|
|
176
|
+
+ "Social-Icons, Zeile ergänzen). Sucht `find` wörtlich im Quelltext (wie site_file_get) innerhalb von region (footer | header | "
|
|
177
|
+
+ "main | all) und ersetzt durch `replace` – EIN Rückgängig-Schritt für alle Seiten. scope:\"all\" = alle Fundstellen ersetzen; ohne "
|
|
178
|
+
+ "scope ändert der Server bei mehr als einer Fundstelle NICHTS und liefert die Liste (oder pages:[…]). Für reinen Text nimm "
|
|
179
|
+
+ "site_edit_text. Rechtstext-Bereich, Skripte (Regeln wie site_file_put) und das ProntoSite-Badge sind geschützt (skipped).",
|
|
180
|
+
{ site: z.string().optional().describe(SITE_DESC),
|
|
181
|
+
find: z.string().describe("Exaktes Markup, wie es im Quelltext steht (aus site_file_get kopieren)."),
|
|
182
|
+
replace: z.string().describe("Neues Markup."),
|
|
183
|
+
region: z.enum(["all", "header", "main", "footer"]).optional().describe("Nur in diesem Bereich suchen (erstes <footer>/<header>/<main> je Seite)."),
|
|
184
|
+
scope: z.enum(["all"]).optional().describe("\"all\" = alle Fundstellen auf allen (bzw. den gewählten) Seiten ersetzen."),
|
|
185
|
+
pages: z.array(z.string()).optional().describe("Nur diese Seiten (ID, Datei oder Slug)."),
|
|
186
|
+
baseRev: z.number().int().optional().describe("Optional: draftRev aus site_status (Konfliktschutz).") },
|
|
187
|
+
async (a) => { try { return asText(await apiEditHtml(cfg, a)); } catch (e) { return asError(e); } }
|
|
188
|
+
);
|
|
168
189
|
server.tool(
|
|
169
190
|
"site_page_texts",
|
|
170
191
|
"LESEND – STANDARDWEG für Übersetzung/Umformulierung: liefert ALLE Texte einer Seite ohne HTML: [{id, role, text, markdown?}] – "
|
|
@@ -232,6 +253,31 @@ export function registerTools(server0, { z, cfg, mode = "local", remote = null }
|
|
|
232
253
|
{ site: z.string().optional().describe(SITE_DESC), uploadId: z.string().describe("uploadId aus media_upload_begin.") },
|
|
233
254
|
async ({ site, uploadId }) => { try { return asText(await apiMediaCommit(cfg, { site, uploadId })); } catch (e) { return asError(e); } }
|
|
234
255
|
);
|
|
256
|
+
server.tool(
|
|
257
|
+
"media_search",
|
|
258
|
+
"Kostenlose Fotos von Unsplash suchen (verbraucht ein gemeinsames Stunden-Kontingent; gleiche Suchen kommen 1 h aus dem Cache). "
|
|
259
|
+
+ "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.",
|
|
261
|
+
{ site: z.string().optional().describe(SITE_DESC), query: z.string().describe("Suchbegriff (2–120 Zeichen)."),
|
|
262
|
+
orientation: z.enum(["landscape", "portrait", "squarish"]).optional().describe("Ausrichtung: landscape (Hero/Banner), portrait, squarish."),
|
|
263
|
+
count: z.number().int().min(1).max(6).default(3).describe("Anzahl Treffer (max. 6).") },
|
|
264
|
+
async ({ site, query, orientation, count }) => {
|
|
265
|
+
try {
|
|
266
|
+
const r = await apiMediaSearch(cfg, { site, query, orientation, count });
|
|
267
|
+
const images = (r.results || []).filter(x => x.preview?.data).map(x => ({ type: "image", data: x.preview.data, mimeType: x.preview.mimeType || "image/jpeg" }));
|
|
268
|
+
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] };
|
|
270
|
+
} catch (e) { return asError(e); }
|
|
271
|
+
}
|
|
272
|
+
);
|
|
273
|
+
server.tool(
|
|
274
|
+
"media_import",
|
|
275
|
+
"SCHREIBEND: einen Unsplash-Treffer aus media_search übernehmen – der Server lädt das Foto (Download-Trigger laut Unsplash-Richtlinie), "
|
|
276
|
+
+ "erzeugt Varianten, speichert Fotograf/Quelle in den Medien-Metadaten und liefert mediaId + vorbelegten alt-Text. Zählt aufs "
|
|
277
|
+
+ "Medien-Kontingent. Danach site_image_set {mediaId, alt} und site_preview_screenshot.",
|
|
278
|
+
{ site: z.string().optional().describe(SITE_DESC), resultId: z.string().describe("resultId aus media_search (1 h gültig).") },
|
|
279
|
+
async ({ site, resultId }) => { try { return asText(await apiMediaImport(cfg, { site, resultId })); } catch (e) { return asError(e); } }
|
|
280
|
+
);
|
|
235
281
|
server.tool(
|
|
236
282
|
"site_media_list",
|
|
237
283
|
"LESEND: Medien der Website (mediaId, Name, Typ, Größe, Maße; Uploads und Bilder der importierten Website) und das Medien-Kontingent; trash = Papierkorb (trashId für site_media_restore).",
|