@prontosite/mcp 0.13.5 → 0.13.7
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 +20 -3
- package/index.mjs +1 -1
- package/package.json +1 -1
- package/tools.mjs +49 -6
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.13.7 – 2026-10-20
|
|
7
|
+
|
|
8
|
+
- Neu: `site_forms_get` – Konto-Empfänger, je Formular Art (verwaltetes ProntoSite-Formular oder eigenes `<form>` aus dem
|
|
9
|
+
Import), Seiten, Felder, eigene Empfänger, Danke-Seite; Spam-Modus (wirksam, Kunde, Admin-Übersteuerung).
|
|
10
|
+
- Neu: `site_forms_set {recipients?, form?, spamMode?, thanksPage?}` – Empfänger (Konto oder je Formular, streng geprüft, kein
|
|
11
|
+
CRLF), Spam-Modus `streng|normal|basis`, Danke-Seite nur als interne Seite. Verlauf mit Akteur + Client (alt → neu).
|
|
12
|
+
- `site_status.forms`: Anzahl, Konto-Empfänger, Formulare mit eigenen Empfängern, wirksamer Spam-Modus.
|
|
13
|
+
- Instructions: „Formular-Empfänger, Spam-Stufe, Danke-Seite über site_forms_set, nie per HTML."
|
|
14
|
+
- Neue Empfänger-Adressen über KI-Clients werden erst nach Bestätigung durch die Adresse aktiv (`ausstehend`, 24 h); bisherige
|
|
15
|
+
Adressen bekommen einen Hinweis mit Zurücksetzen-Link.
|
|
16
|
+
|
|
17
|
+
## 0.13.6 – 2026-10-20
|
|
18
|
+
|
|
19
|
+
- Websites beim Namen nennen: `site` darf jetzt auch Firmenname, Domain oder Site-Titel sein (unscharf: Groß/Klein, Umlaute,
|
|
20
|
+
Bindestriche, Teilstring). Eindeutig → ausgeführt, die Antwort nennt den aufgelösten slug (`aufgeloesteWebsite`); mehrdeutig →
|
|
21
|
+
Kandidatenliste, nichts geändert; kein Treffer → die drei ähnlichsten. Nur innerhalb der erreichbaren Websites.
|
|
22
|
+
- `sites_list`: je Website Firmenname (`company`), Site-Titel (`title`), Domains, Status (live/entwurf), Kunde-ID.
|
|
23
|
+
- `site_status`: konkrete Rechte des Aufrufers – `canRead`, `canEditDraft`, `canPublish`, `canDeploy` (+ `canReasons`);
|
|
24
|
+
`apiEnabled` → `restApiEnabled` (betrifft nur die REST-API).
|
|
25
|
+
- Tool-Annotationen (`readOnlyHint`, `destructiveHint`, `openWorldHint`) auf allen Tools; neue Instructions zu Namen und Rechten.
|
|
26
|
+
|
|
6
27
|
## 0.13.5 – 2026-10-20
|
|
7
28
|
|
|
8
29
|
- `site_edit_text`: Telefonnummern werden unabhängig von der Schreibweise gefunden (Leerzeichen, „/", Bindestrich-Varianten,
|
package/api.mjs
CHANGED
|
@@ -124,7 +124,11 @@ export function friendlyApiError(status, serverError = "") {
|
|
|
124
124
|
async function asJson(res) {
|
|
125
125
|
let body = {};
|
|
126
126
|
try { body = await res.json(); } catch { /* kein JSON */ }
|
|
127
|
-
if (!res.ok)
|
|
127
|
+
if (!res.ok) {
|
|
128
|
+
// Website-Auflösung (Firmenname/Domain): Kandidaten bzw. Vorschläge in die Meldung, damit der Assistent nachfragen kann.
|
|
129
|
+
const list = (body.candidates || body.suggestions || []).map(e => `${e.slug || e.site}${e.name ? ` (${e.name})` : ""}`).join(", ");
|
|
130
|
+
throw new Error(friendlyApiError(res.status, body.error) + (list ? ` ${body.candidates ? "Kandidaten" : "Ähnlich"}: ${list}.` : ""));
|
|
131
|
+
}
|
|
128
132
|
return body;
|
|
129
133
|
}
|
|
130
134
|
|
|
@@ -316,10 +320,11 @@ export async function apiLegalOptions(cfg, { site, impressum, datenschutz } = {}
|
|
|
316
320
|
async function apiSitesWithToken(cfg, token, fetchImpl) {
|
|
317
321
|
const body = await asJson(await fetchImpl(`${baseUrl(cfg)}/sites`, { headers: authHeaders(token, cfg) }));
|
|
318
322
|
const sites = (body.sites || []).map(s => ({ label: s.slug || s.site || s.customer, slug: s.slug || null, id: s.site || null,
|
|
319
|
-
name: s.name || null,
|
|
323
|
+
name: s.name || null, company: s.company || null, title: s.title || null, domains: s.domains || [], status: s.status || null,
|
|
324
|
+
plan: s.plan, everPublished: s.everPublished, publishNeeded: s.publishNeeded,
|
|
320
325
|
lastChangeAt: s.lastChangeAt || null, liveUrl: s.liveUrl || null, customer: s.customer,
|
|
321
326
|
...(s.needsFirstImport ? { needsFirstImport: true, hint: s.hint || null } : {}) }));
|
|
322
|
-
return { count: sites.length, manager: !!body.manager, hint: body.hint || null, sites };
|
|
327
|
+
return { count: sites.length, manager: !!body.manager, hint: body.hint || null, ...(body.hinweis ? { hinweis: body.hinweis } : {}), sites };
|
|
323
328
|
}
|
|
324
329
|
// sites-Map abfragen: jeden Eintrag einzeln (jeder Token → seine Site).
|
|
325
330
|
async function sitesFromMap(cfg, fetchImpl) {
|
|
@@ -515,6 +520,18 @@ export async function apiImageSet(cfg, { site, page, nodeId = null, marker = nul
|
|
|
515
520
|
method: "POST", headers: { ...authHeaders(token, cfg), "content-type": "application/json" },
|
|
516
521
|
body: JSON.stringify({ page, mediaId, ...(nodeId ? { nodeId } : {}), ...(marker ? { marker } : {}), ...(alt != null ? { alt } : {}), ...(Number.isInteger(baseRev) ? { baseRev } : {}) }) }));
|
|
517
522
|
}
|
|
523
|
+
/** GET /api/forms/<site> – Formulare: Konto-Empfänger, Empfänger je Formular, Spam-Modus, Danke-Seiten, erkannte Formulare. */
|
|
524
|
+
export async function apiFormsGet(cfg, { site } = {}, fetchImpl = fetch) {
|
|
525
|
+
const { token, slug } = resolveTarget(cfg, site);
|
|
526
|
+
return asJson(await fetchImpl(`${baseUrl(cfg)}/forms/${encodeURIComponent(slug)}`, { headers: authHeaders(token, cfg) }));
|
|
527
|
+
}
|
|
528
|
+
/** POST /api/forms/<site> – Empfänger (Konto oder je Formular), Spam-Modus, Danke-Seite setzen. */
|
|
529
|
+
export async function apiFormsSet(cfg, { site, form = null, recipients, spamMode, thanksPage } = {}, fetchImpl = fetch) {
|
|
530
|
+
const { token, slug } = resolveTarget(cfg, site);
|
|
531
|
+
const body = { ...(form ? { form } : {}), ...(recipients !== undefined ? { recipients } : {}), ...(spamMode !== undefined ? { spamMode } : {}), ...(thanksPage !== undefined ? { thanksPage } : {}) };
|
|
532
|
+
return asJson(await fetchImpl(`${baseUrl(cfg)}/forms/${encodeURIComponent(slug)}`, {
|
|
533
|
+
method: "POST", headers: { ...authHeaders(token, cfg), "content-type": "application/json" }, body: JSON.stringify(body) }));
|
|
534
|
+
}
|
|
518
535
|
/** POST /api/undo|redo/<site> – letzte Änderung(en) des Entwurfs zurücknehmen bzw. wiederherstellen (derselbe Stack wie das Dashboard). */
|
|
519
536
|
export async function apiUndo(cfg, { site, steps = 1, baseRev = null, redo = false } = {}, fetchImpl = fetch) {
|
|
520
537
|
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.13.
|
|
24
|
+
const server = new McpServer({ name: "prontosite", version: "0.13.7" }, { 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.13.
|
|
3
|
+
"version": "0.13.7",
|
|
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, 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, 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";
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* Arbeitsweise (MCP-`instructions` beim Initialize – lokal UND /mcp): Weg B ist der Standard (Nick 19.10.).
|
|
@@ -20,6 +20,10 @@ const instructionLines = mode => [
|
|
|
20
20
|
"1. STANDARD: direkt am Server-Entwurf arbeiten. Ablauf: site_status (Stand, draftRev, wer zuletzt geändert hat) → Änderung mit "
|
|
21
21
|
+ "site_edit_text (Text suchen und ersetzen über alle Seiten) bzw. site_file_get/site_file_put (HTML/CSS) → site_status (Kontrolle). "
|
|
22
22
|
+ "Danach dem Nutzer sagen, was geändert wurde, und den Vorschau-Link (previewUrl aus site_status) nennen.",
|
|
23
|
+
"1a. Website wählen: Wenn der Nutzer eine Website beim Firmennamen nennt, erst sites_list, dann den passenden Slug verwenden; bei "
|
|
24
|
+
+ "Unklarheit nachfragen. (Die Tools lösen Firmenname/Domain/Titel auch selbst auf; mehrdeutig → Kandidatenliste, nichts geändert.)",
|
|
25
|
+
"1b. Rechte aus site_status.can* lesen (canRead, canEditDraft, canPublish, canDeploy, Gründe in canReasons); nicht aus anderen Feldern "
|
|
26
|
+
+ "schließen – restApiEnabled betrifft nur die REST-API, nicht diese Tools.",
|
|
23
27
|
"2. AUSNAHME: site_export + Deploy (site_deploy bzw. deploy_upload_begin/deploy_commit) NUR, wenn der Nutzer ausdrücklich lokal arbeiten "
|
|
24
28
|
+ "möchte oder die Website komplett umgebaut wird.",
|
|
25
29
|
"2a. Für Text-, Link- und Kontaktdaten-Änderungen (Telefonnummer, E-Mail, Adresse, Öffnungszeiten) IMMER site_edit_text mit "
|
|
@@ -40,22 +44,38 @@ const instructionLines = mode => [
|
|
|
40
44
|
+ "site_preview_screenshot (1280 bzw. 390) prüfen, BEVOR du dem Nutzer antwortest.",
|
|
41
45
|
"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 "
|
|
42
46
|
+ "(Papierkorb, 30 Tage; zurückholen mit site_media_restore).",
|
|
47
|
+
"5d. Formular-Empfänger, Spam-Stufe und Danke-Seite über site_forms_set, nie per HTML (site_forms_get zeigt den Stand).",
|
|
43
48
|
"5. Jede Änderung lässt sich rückgängig machen; Rechtstexte (Impressum/Datenschutz) nicht umformulieren – sie kommen aus den Firmendaten.",
|
|
44
49
|
];
|
|
45
50
|
/** Server-Instructions je Modus ("local" = stdio-Paket mit Dateizugriff, "remote" = /mcp). */
|
|
46
51
|
export const instructionsFor = mode => instructionLines(mode).join("\n");
|
|
47
52
|
export const SERVER_INSTRUCTIONS = instructionsFor("remote");
|
|
48
53
|
|
|
54
|
+
// Tool-Annotationen (MCP ToolAnnotations): ChatGPT & Co. fragen sonst bei JEDEM Aufruf nach. readOnly = ändert nichts;
|
|
55
|
+
// destructive = live/kundenwirksam, ersetzt Bestand oder nimmt zurück (Bestätigung sinnvoll). Entwurfs-Änderungen sind
|
|
56
|
+
// 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",
|
|
58
|
+
"site_domain_status", "site_embeds", "site_widgets", "site_files", "site_file_get"]);
|
|
59
|
+
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"]);
|
|
61
|
+
export function toolAnnotations(name, mode = "local") {
|
|
62
|
+
// site_export: remote nur ein Download-Link (lesend), lokal schreibt es eine ZIP auf die Platte.
|
|
63
|
+
const ro = READ_ONLY.has(name) || (name === "site_export" && mode === "remote");
|
|
64
|
+
return { readOnlyHint: ro, destructiveHint: !ro && DESTRUCTIVE.has(name), idempotentHint: ro || name === "site_lock" || name === "site_unlock", openWorldHint: OPEN_WORLD.has(name) };
|
|
65
|
+
}
|
|
66
|
+
|
|
49
67
|
const asText = obj => ({ content: [{ type: "text", text: JSON.stringify(obj, null, 2) }] });
|
|
50
68
|
const asError = e => ({ isError: true, content: [{ type: "text", text: "Fehler: " + (e?.message || String(e)) }] });
|
|
51
69
|
|
|
52
70
|
/** Alle Tools am MCP-Server registrieren. */
|
|
53
|
-
export function registerTools(
|
|
71
|
+
export function registerTools(server0, { z, cfg, mode = "local", remote = null }) {
|
|
72
|
+
// Jedes Tool mit Annotationen registrieren (Signatur des SDK: name, description, schema, annotations, handler).
|
|
73
|
+
const server = { tool: (name, desc, schema, cb) => server0.tool(name, desc, schema, toolAnnotations(name, mode), cb) };
|
|
54
74
|
const SITE_DESC = mode === "remote"
|
|
55
|
-
? "Website
|
|
56
|
-
+ "die gewünschte Website angeben (sites_list zeigt alle)."
|
|
57
|
-
: "Website
|
|
58
|
-
+ "Config (mehrere Sites) hier die gewünschte Website angeben.";
|
|
75
|
+
? "Website: Slug (z. B. 'mas-monti'), Site-ID (s-…), Firmenname, Domain oder Site-Titel (unscharf, z. B. 'Firat-Deveci'). Ohne Angabe "
|
|
76
|
+
+ "die Website des Tokens bzw. der Freigabe. Betreuer: die gewünschte Website angeben (sites_list zeigt alle mit Firmennamen)."
|
|
77
|
+
: "Website: Slug (z. B. 'mas-monti'), Firmenname, Domain oder Site-Titel (unscharf). Ohne Angabe die Standard-Site der Config. "
|
|
78
|
+
+ "Bei Betreuer-Config (mehrere Sites) hier die gewünschte Website angeben.";
|
|
59
79
|
|
|
60
80
|
// Firmendaten-Felder (identisch zum Dashboard-Formular / Sanitizer). Nur gesetzte Felder werden geändert.
|
|
61
81
|
const companyObject = z.object({
|
|
@@ -228,6 +248,29 @@ export function registerTools(server, { z, cfg, mode = "local", remote = null })
|
|
|
228
248
|
baseRev: z.number().int().optional().describe("Optional: draftRev (Konfliktschutz).") },
|
|
229
249
|
async (a) => { try { return asText(await apiImageSet(cfg, a)); } catch (e) { return asError(e); } }
|
|
230
250
|
);
|
|
251
|
+
server.tool(
|
|
252
|
+
"site_forms_get",
|
|
253
|
+
"LESEND: Formulare der Website – Konto-Empfänger (defaultRecipients), je Formular: Art (verwaltetes ProntoSite-Formular/Marker "
|
|
254
|
+
+ "oder eigenes <form> aus dem Import), Seiten, Felder, eigene Empfänger (null = Konto-Empfänger), Danke-Seite; Spam-Modus "
|
|
255
|
+
+ "(wirksam, Kunde, Admin-Übersteuerung) und die möglichen Danke-Seiten.",
|
|
256
|
+
{ site: z.string().optional().describe(SITE_DESC) },
|
|
257
|
+
async ({ site }) => { try { return asText(await apiFormsGet(cfg, { site })); } catch (e) { return asError(e); } }
|
|
258
|
+
);
|
|
259
|
+
server.tool(
|
|
260
|
+
"site_forms_set",
|
|
261
|
+
"SCHREIBEND: Formular-Einstellungen ändern – NIE per HTML. recipients: Empfänger-Adressen (ohne form = Konto-Empfänger für alle "
|
|
262
|
+
+ "Formulare ohne eigene; mit form = nur dieses Formular, [] = zurück auf Konto-Empfänger); spamMode: streng | normal | basis; "
|
|
263
|
+
+ "thanksPage: interne Seite für die Weiterleitung nach dem Absenden ('' = Meldung am Formular), nur mit form (außer es gibt genau "
|
|
264
|
+
+ "eins). NEUE Empfänger-Adressen werden erst aktiv, wenn sie den Bestätigungslink in der Mail an sie anklicken (24 h) – bis dahin "
|
|
265
|
+
+ "bleiben die bisherigen aktiv (Antwort: ausstehend). Die bisherigen Adressen bekommen einen Hinweis mit Zurücksetzen-Link. Vorher "
|
|
266
|
+
+ "beim Nutzer bestätigen, wohin Anfragen künftig gehen.",
|
|
267
|
+
{ site: z.string().optional().describe(SITE_DESC),
|
|
268
|
+
form: z.string().optional().describe("Formular-ID aus site_forms_get (für Empfänger/Danke-Seite je Formular)."),
|
|
269
|
+
recipients: z.array(z.string()).optional().describe("E-Mail-Adressen (max. 10)."),
|
|
270
|
+
spamMode: z.enum(["streng", "normal", "basis"]).optional(),
|
|
271
|
+
thanksPage: z.string().optional().describe("Interner Pfad einer Seite dieser Website, z. B. /danke/ (keine externen URLs).") },
|
|
272
|
+
async (a) => { try { return asText(await apiFormsSet(cfg, a)); } catch (e) { return asError(e); } }
|
|
273
|
+
);
|
|
231
274
|
server.tool(
|
|
232
275
|
"site_undo",
|
|
233
276
|
"SCHREIBEND (Entwurf): macht die letzte(n) Änderung(en) rückgängig – derselbe Verlauf wie der Rückgängig-Knopf im Dashboard (auch "
|