@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 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
- return {
31
- host,
32
- token: env.PRONTOSITE_TOKEN || file.token || "",
33
- site: env.PRONTOSITE_SITE || file.site || "active",
34
- sites,
35
- defaultSite: env.PRONTOSITE_SITE || file.defaultSite || null,
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
- export async function sitesList(cfg, fetchImpl = fetch) {
183
- if (cfg.sites) {
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
- const server = new McpServer({ name: "prontosite", version: "0.3.1" });
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.5.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": { "prontosite-mcp": "index.mjs" },
6
+ "bin": {
7
+ "prontosite-mcp": "index.mjs"
8
+ },
7
9
  "main": "index.mjs",
8
- "files": ["index.mjs", "api.mjs", "README.md", "CHANGELOG.md"],
9
- "engines": { "node": ">=18" },
10
- "keywords": ["prontosite", "mcp", "model-context-protocol", "claude", "deploy", "website"],
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": { "access": "public" },
29
+ "publishConfig": {
30
+ "access": "public"
31
+ },
14
32
  "dependencies": {
15
33
  "@modelcontextprotocol/sdk": "^1.0.0",
16
34
  "zod": "^3.23.0"