@prontosite/mcp 0.5.0 → 0.7.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/README.md +2 -0
- package/api.mjs +67 -23
- package/index.mjs +32 -2
- package/package.json +24 -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.7.0 – 2026-09-23
|
|
7
|
+
|
|
8
|
+
- **Ambige Config erkannt:** Liegt ein Top-Level-Token (env `PRONTOSITE_TOKEN` bzw. `token`) NEBEN einer
|
|
9
|
+
`sites`-Map, warnt der Server beim Start deutlich auf stderr und `sites_list` trägt den Hinweis mit:
|
|
10
|
+
„Beide Konfigurationen gefunden — sites-Map wird genutzt, env-Token ignoriert."
|
|
11
|
+
- **Betreuer-Token hat IMMER Vorrang:** Ist das Top-Level-Token ein Betreuer-Token (scope=manager), nutzt
|
|
12
|
+
`sites_list` dieses (deckt alle Kunden ab) und meldet die daneben liegende `sites`-Map als
|
|
13
|
+
Konfigurationsfehler (Map ignoriert). Ein Einzel-Token wird weiterhin von der Map verdrängt.
|
|
14
|
+
- **`liveUrl` = kanonische Kundendomain**, sobald `customDomains` gesetzt ist (sonst die Slug-Subdomain) –
|
|
15
|
+
in `sites_list` und `site_status`. So wird gedanklich nie „auf die falsche Adresse" deployt; die Slug-URL
|
|
16
|
+
leitet ohnehin per 301 auf die Kundendomain weiter.
|
|
17
|
+
|
|
18
|
+
## 0.6.0 – 2026-09-22
|
|
19
|
+
|
|
20
|
+
- **Neu: `site_domain_status`** – Kundendomain-Status (verbunden/kanonisch/Zertifikat-bis) + offene Jobs;
|
|
21
|
+
mit `check` nur eine Live-DNS-Prüfung (A @+www → Server-IP, keine AAAA) vor dem Verbinden. LESEND.
|
|
22
|
+
- **Neu: `site_domain_connect`** (SCHREIBEND) – eigene Domain verbinden (nginx-Block + HTTPS-Zertifikat
|
|
23
|
+
automatisch, kanonisch `www`/`apex`, inkl. Republish) oder trennen (`action:disconnect`). Voraussetzung:
|
|
24
|
+
DNS zeigt bereits auf den Server (vorher `site_domain_status` mit `check`). Ab Starter-Plan (Betreuer-/
|
|
25
|
+
Operator-Token ausgenommen); eine Domain nur an EINER Website. Läuft asynchron über die Job-Queue.
|
|
26
|
+
|
|
6
27
|
## 0.5.0 – 2026-09-20
|
|
7
28
|
|
|
8
29
|
- **Neu: `site_legal_options`** – Anzeige-Optionen der Rechtstext-Widgets je Rolle (impressum/datenschutz):
|
package/README.md
CHANGED
|
@@ -89,6 +89,8 @@ sichern: `cp ~/.prontosite.json ~/.prontosite.json.bak`).
|
|
|
89
89
|
| `site_company_get({ site? })` | Firmendaten (Impressum/Footer) lesen. **Lesend.** |
|
|
90
90
|
| `site_company_set({ site?, company, siteOverride? })` | Firmendaten setzen/ergänzen (Teil-Update); füllen Impressum/Datenschutz beim Deploy. **Schreibend.** |
|
|
91
91
|
| `site_legal_options({ site?, impressum?, datenschutz? })` | Anzeige-Optionen der Rechtstexte (showTitle, showDate, draftNotice); ohne Rolle nur lesen. **Schreibend.** |
|
|
92
|
+
| `site_domain_status({ site?, check? })` | Kundendomain-Status (verbunden/kanonisch/Zert-bis) + offene Jobs; mit `check` nur Live-DNS-Prüfung. **Lesend.** |
|
|
93
|
+
| `site_domain_connect({ site?, domain, canonical?, action? })` | Eigene Domain verbinden (nginx + HTTPS automatisch, `www`/`apex`) oder trennen; DNS muss auf den Server zeigen; ab Starter-Plan. **Schreibend.** |
|
|
92
94
|
|
|
93
95
|
`site_deploy` und `site_publish` sind **schreibende** Aktionen (verändern die Live-Website). Die
|
|
94
96
|
Tool-Beschreibungen weisen den Assistenten an, vorher rückzufragen. Jeder Aufruf wird serverseitig auditiert.
|
package/api.mjs
CHANGED
|
@@ -27,13 +27,19 @@ export function loadConfig(env = process.env, home = os.homedir()) {
|
|
|
27
27
|
} catch { /* defekte Datei ignorieren – Env kann trotzdem reichen */ }
|
|
28
28
|
const host = (env.PRONTOSITE_HOST || file.host || DEFAULT_HOST).replace(/^https?:\/\//, "").replace(/\/+$/, "");
|
|
29
29
|
const sites = (file.sites && typeof file.sites === "object") ? file.sites : null;
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
30
|
+
const envToken = env.PRONTOSITE_TOKEN || "";
|
|
31
|
+
const token = envToken || file.token || "";
|
|
32
|
+
const tokenSource = envToken ? "env" : (file.token ? "datei" : null);
|
|
33
|
+
// Ambige Konfiguration: ein Top-Level-Token (Einzel/Betreuer) NEBEN einer sites-Map. Auflösung:
|
|
34
|
+
// Ein Betreuer-Token (scope=manager) hat IMMER Vorrang (die sites-Map ist dann ein Konfigurationsfehler);
|
|
35
|
+
// ein Einzel-Token wird von der Map verdrängt. Ob es ein Betreuer-Token ist, weiß nur der Server →
|
|
36
|
+
// sites_list klärt es per Server-Antdwort; hier nur die generische Startwarnung.
|
|
37
|
+
const warnings = [];
|
|
38
|
+
if (sites && token) warnings.push(
|
|
39
|
+
`Beide Konfigurationen gefunden — sites-Map wird genutzt, ${tokenSource === "env" ? "env-Token" : "Token"} ignoriert. `
|
|
40
|
+
+ `(Ausnahme: ein Betreuer-Token hat Vorrang – eine sites-Map daneben ist ein Konfigurationsfehler; bitte eins entfernen.)`);
|
|
41
|
+
return { host, token, tokenSource, site: env.PRONTOSITE_SITE || file.site || "active", sites,
|
|
42
|
+
defaultSite: env.PRONTOSITE_SITE || file.defaultSite || null, warnings };
|
|
37
43
|
}
|
|
38
44
|
|
|
39
45
|
const needToken = (token, ref) => {
|
|
@@ -162,6 +168,27 @@ export async function apiCompanySet(cfg, { site, company, siteOverride = false }
|
|
|
162
168
|
return asJson(res);
|
|
163
169
|
}
|
|
164
170
|
|
|
171
|
+
/** GET /api/domain/<site> – Kundendomain-Status (verbunden/kanonisch/Zertifikat-bis) + offene Jobs. Mit
|
|
172
|
+
* check=<domain> stattdessen NUR die Live-DNS-Prüfung (A für @ und www → Server-IP, keine AAAA-Leichen). */
|
|
173
|
+
export async function apiDomainStatus(cfg, { site, check } = {}, fetchImpl = fetch) {
|
|
174
|
+
const { token, slug } = resolveTarget(cfg, site);
|
|
175
|
+
const qs = check ? `?check=${encodeURIComponent(check)}` : "";
|
|
176
|
+
const res = await fetchImpl(`${baseUrl(cfg)}/domain/${encodeURIComponent(slug)}${qs}`, { headers: authHeaders(token) });
|
|
177
|
+
return asJson(res);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** POST /api/domain/<site> – SCHREIBEND: eigene Domain verbinden (nginx-Block + HTTPS-Zertifikat werden
|
|
181
|
+
* automatisch eingerichtet) oder trennen. Voraussetzung: der DNS zeigt bereits auf den Server – vorher
|
|
182
|
+
* apiDomainStatus mit `check` nutzen. Läuft asynchron über die Job-Queue; Ergebnis via apiDomainStatus. */
|
|
183
|
+
export async function apiDomainConnect(cfg, { site, domain, canonical = "www", action = "connect" } = {}, fetchImpl = fetch) {
|
|
184
|
+
if (!domain) throw new Error("domain erwartet.");
|
|
185
|
+
const { token, slug } = resolveTarget(cfg, site);
|
|
186
|
+
const res = await fetchImpl(`${baseUrl(cfg)}/domain/${encodeURIComponent(slug)}`, {
|
|
187
|
+
method: "POST", headers: { ...authHeaders(token), "content-type": "application/json" },
|
|
188
|
+
body: JSON.stringify({ domain, canonical, action }) });
|
|
189
|
+
return asJson(res);
|
|
190
|
+
}
|
|
191
|
+
|
|
165
192
|
/** GET/PUT /api/legal-options/<site> – Anzeige-Optionen der Rechtstext-Widgets. Ohne impressum/datenschutz
|
|
166
193
|
* nur LESEN; sonst Teil-Update (nur echte Booleans wirken). */
|
|
167
194
|
export async function apiLegalOptions(cfg, { site, impressum, datenschutz } = {}, fetchImpl = fetch) {
|
|
@@ -179,24 +206,41 @@ export async function apiLegalOptions(cfg, { site, impressum, datenschutz } = {}
|
|
|
179
206
|
|
|
180
207
|
/** Alle erreichbaren Sites auflisten (Slug, Plan, Publish-Bedarf, letzte Änderung). Bei einer sites-Map wird
|
|
181
208
|
* jeder Eintrag per Status abgefragt; sonst die eine konfigurierte Site. Fehler je Site werden mitgeführt. */
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
// Per-Kunde-Token-Map: jeden Eintrag einzeln abfragen (jeder Token → seine Site).
|
|
185
|
-
const out = await Promise.all(Object.keys(cfg.sites).map(async label => {
|
|
186
|
-
try {
|
|
187
|
-
const s = await apiStatus(cfg, { site: label }, fetchImpl);
|
|
188
|
-
return { label, slug: s.slug || null, id: s.site || null, name: s.name || null, plan: s.plan,
|
|
189
|
-
everPublished: s.everPublished, publishNeeded: s.publishNeeded, lastChangeAt: s.lastChangeAt || null, liveUrl: s.liveUrl || null };
|
|
190
|
-
} catch (e) { return { label, error: e.message }; }
|
|
191
|
-
}));
|
|
192
|
-
return { count: out.length, sites: out };
|
|
193
|
-
}
|
|
194
|
-
// Einzel-/Betreuer-Token: der Server liefert alle erreichbaren Sites (Betreuer-Token: ALLE betreuten Kunden,
|
|
195
|
-
// ohne Config-Pflege). GET /api/sites.
|
|
196
|
-
const { token } = resolveTarget(cfg, cfg.defaultSite || cfg.site || "active");
|
|
209
|
+
// GET /api/sites mit EINEM Token → normalisierte Liste (Betreuer-Token: alle betreuten Kunden).
|
|
210
|
+
async function apiSitesWithToken(cfg, token, fetchImpl) {
|
|
197
211
|
const body = await asJson(await fetchImpl(`${baseUrl(cfg)}/sites`, { headers: authHeaders(token) }));
|
|
198
212
|
const sites = (body.sites || []).map(s => ({ label: s.slug || s.site, slug: s.slug || null, id: s.site || null,
|
|
199
213
|
name: s.name || null, plan: s.plan, everPublished: s.everPublished, publishNeeded: s.publishNeeded,
|
|
200
214
|
lastChangeAt: s.lastChangeAt || null, liveUrl: s.liveUrl || null, customer: s.customer }));
|
|
201
|
-
return { count: sites.length, manager: !!body.manager, sites };
|
|
215
|
+
return { count: sites.length, manager: !!body.manager, hint: body.hint || null, sites };
|
|
216
|
+
}
|
|
217
|
+
// sites-Map abfragen: jeden Eintrag einzeln (jeder Token → seine Site).
|
|
218
|
+
async function sitesFromMap(cfg, fetchImpl) {
|
|
219
|
+
const out = await Promise.all(Object.keys(cfg.sites).map(async label => {
|
|
220
|
+
try {
|
|
221
|
+
const s = await apiStatus(cfg, { site: label }, fetchImpl);
|
|
222
|
+
return { label, slug: s.slug || null, id: s.site || null, name: s.name || null, plan: s.plan,
|
|
223
|
+
everPublished: s.everPublished, publishNeeded: s.publishNeeded, lastChangeAt: s.lastChangeAt || null, liveUrl: s.liveUrl || null };
|
|
224
|
+
} catch (e) { return { label, error: e.message }; }
|
|
225
|
+
}));
|
|
226
|
+
return { count: out.length, sites: out };
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export async function sitesList(cfg, fetchImpl = fetch) {
|
|
230
|
+
// Ambig (Top-Level-Token NEBEN sites-Map): Betreuer-Token hat IMMER Vorrang. Nur der Server kennt den
|
|
231
|
+
// Scope → das Top-Level-Token einmal prüfen: manager:true → dieses nutzen (Map ist Konfigurationsfehler);
|
|
232
|
+
// sonst die Map (Einzel-Token verdrängt).
|
|
233
|
+
if (cfg.sites && cfg.token) {
|
|
234
|
+
let probe = null;
|
|
235
|
+
try { probe = await apiSitesWithToken(cfg, cfg.token, fetchImpl); } catch { /* Token ungültig? → Map-Weg */ }
|
|
236
|
+
if (probe && probe.manager)
|
|
237
|
+
return { ...probe, configError: "Konfigurationsfehler: Betreuer-Token UND sites-Map konfiguriert. "
|
|
238
|
+
+ "Der Betreuer-Token deckt bereits ALLE betreuten Kunden ab und hat Vorrang – die sites-Map wird ignoriert. "
|
|
239
|
+
+ "Bitte die sites-Map (oder den Betreuer-Token) aus ~/.prontosite.json entfernen." };
|
|
240
|
+
return { ...(await sitesFromMap(cfg, fetchImpl)), warning: cfg.warnings[0] || null };
|
|
241
|
+
}
|
|
242
|
+
if (cfg.sites) return sitesFromMap(cfg, fetchImpl);
|
|
243
|
+
// Einzel-/Betreuer-Token ohne Map: der Server liefert alle erreichbaren Sites.
|
|
244
|
+
const { token } = resolveTarget(cfg, cfg.defaultSite || cfg.site || "active");
|
|
245
|
+
return apiSitesWithToken(cfg, token, fetchImpl);
|
|
202
246
|
}
|
package/index.mjs
CHANGED
|
@@ -11,10 +11,13 @@
|
|
|
11
11
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
12
12
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
13
13
|
import { z } from "zod";
|
|
14
|
-
import { loadConfig, apiStatus, apiExport, apiDeploy, apiPublish, sitesList, apiCompanyGet, apiCompanySet, apiLegalOptions } from "./api.mjs";
|
|
14
|
+
import { loadConfig, apiStatus, apiExport, apiDeploy, apiPublish, sitesList, apiCompanyGet, apiCompanySet, apiLegalOptions, apiDomainStatus, apiDomainConnect } from "./api.mjs";
|
|
15
15
|
|
|
16
16
|
const cfg = loadConfig();
|
|
17
|
-
|
|
17
|
+
// Deutliche Startwarnung bei ambiger Konfiguration (Top-Level-Token NEBEN sites-Map). Nach stderr, damit
|
|
18
|
+
// es im MCP-Log/Terminal auffällt; die endgültige Auflösung (Betreuer-Token-Vorrang vs. Map) klärt sites_list.
|
|
19
|
+
for (const w of (cfg.warnings || [])) process.stderr.write("[prontosite-mcp] ⚠ " + w + "\n");
|
|
20
|
+
const server = new McpServer({ name: "prontosite", version: "0.7.0" });
|
|
18
21
|
const asText = obj => ({ content: [{ type: "text", text: JSON.stringify(obj, null, 2) }] });
|
|
19
22
|
const asError = e => ({ isError: true, content: [{ type: "text", text: "Fehler: " + (e?.message || String(e)) }] });
|
|
20
23
|
const SITE_DESC = "Website-Slug (z. B. 'mas-monti'). Ohne Angabe die Standard-Site der Config. Bei Betreuer-"
|
|
@@ -146,5 +149,32 @@ server.tool(
|
|
|
146
149
|
}
|
|
147
150
|
);
|
|
148
151
|
|
|
152
|
+
server.tool(
|
|
153
|
+
"site_domain_status",
|
|
154
|
+
"LESEND: Kundendomain-Status einer Website – verbundene Domains (kanonisch/Weiterleitung), Zertifikat-"
|
|
155
|
+
+ "Gültigkeit und offene Verbindungs-Jobs. Mit `check` stattdessen NUR eine Live-DNS-Prüfung der angegebenen "
|
|
156
|
+
+ "Domain (A-Record für @ und www muss auf den Server zeigen, kein AAAA) – ideal VOR dem Verbinden.",
|
|
157
|
+
{ site: z.string().optional().describe(SITE_DESC),
|
|
158
|
+
check: z.string().optional().describe("Domain (z. B. ihre-firma.de) NUR auf DNS-Bereitschaft prüfen, nichts verbinden.") },
|
|
159
|
+
async ({ site, check }) => { try { return asText(await apiDomainStatus(cfg, { site, check })); } catch (e) { return asError(e); } }
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
server.tool(
|
|
163
|
+
"site_domain_connect",
|
|
164
|
+
"SCHREIBEND: eigene Domain mit der Website verbinden oder trennen. Beim Verbinden werden nginx-Serverblock "
|
|
165
|
+
+ "und HTTPS-Zertifikat (Let's Encrypt) automatisch eingerichtet und die Domain als kanonische Adresse gesetzt "
|
|
166
|
+
+ "(inkl. Republish für Canonical/Sitemap). VORAUSSETZUNG: der DNS zeigt bereits auf den Server – vorher IMMER "
|
|
167
|
+
+ "site_domain_status mit `check` aufrufen und das Ergebnis bestätigen lassen. Nur ab Starter-Plan (Betreuer-/"
|
|
168
|
+
+ "Operator-Token ausgenommen); eine Domain kann nur an EINER Website hängen. Läuft asynchron über eine Job-"
|
|
169
|
+
+ "Queue – danach site_domain_status pollen. action='disconnect' trennt die Domain wieder.",
|
|
170
|
+
{ site: z.string().optional().describe(SITE_DESC),
|
|
171
|
+
domain: z.string().describe("Die zu verbindende/trennende Domain, z. B. ihre-firma.de (mit oder ohne www)."),
|
|
172
|
+
canonical: z.enum(["www", "apex"]).default("www").describe("Kanonische Adresse: 'www' (mit www) oder 'apex' (ohne www). Nur beim Verbinden relevant."),
|
|
173
|
+
action: z.enum(["connect", "disconnect"]).default("connect").describe("connect = verbinden, disconnect = trennen.") },
|
|
174
|
+
async ({ site, domain, canonical, action }) => {
|
|
175
|
+
try { return asText(await apiDomainConnect(cfg, { site, domain, canonical, action })); } catch (e) { return asError(e); }
|
|
176
|
+
}
|
|
177
|
+
);
|
|
178
|
+
|
|
149
179
|
const transport = new StdioServerTransport();
|
|
150
180
|
await server.connect(transport);
|
package/package.json
CHANGED
|
@@ -1,16 +1,34 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@prontosite/mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.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
|
-
"bin": {
|
|
6
|
+
"bin": {
|
|
7
|
+
"prontosite-mcp": "index.mjs"
|
|
8
|
+
},
|
|
7
9
|
"main": "index.mjs",
|
|
8
|
-
"files": [
|
|
9
|
-
|
|
10
|
-
|
|
10
|
+
"files": [
|
|
11
|
+
"index.mjs",
|
|
12
|
+
"api.mjs",
|
|
13
|
+
"README.md",
|
|
14
|
+
"CHANGELOG.md"
|
|
15
|
+
],
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=18"
|
|
18
|
+
},
|
|
19
|
+
"keywords": [
|
|
20
|
+
"prontosite",
|
|
21
|
+
"mcp",
|
|
22
|
+
"model-context-protocol",
|
|
23
|
+
"claude",
|
|
24
|
+
"deploy",
|
|
25
|
+
"website"
|
|
26
|
+
],
|
|
11
27
|
"license": "UNLICENSED",
|
|
12
28
|
"homepage": "https://prontosite.io/docs/mcp",
|
|
13
|
-
"publishConfig": {
|
|
29
|
+
"publishConfig": {
|
|
30
|
+
"access": "public"
|
|
31
|
+
},
|
|
14
32
|
"dependencies": {
|
|
15
33
|
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
16
34
|
"zod": "^3.23.0"
|