Files
till 30c41c355e v2.3: Feature-Module live — Suche, Merkliste, Kundenkonten+Adressbuch, Bewertungen, Abandoned-Cart
- feature_search: Storefront-Header-Suche + /suche (SSR, SQLite LIKE, case-insensitiv; Name/Kurz/Desc/Material/Kategorie), Treffer als Karten, Leer-Zustand
- feature_wishlist: Herz-Button auf Karten/PDP (localStorage, public/wishlist.js) + /merkliste
- feature_accounts: getrennte Kunden-Session (Cookie hdc_customer, scrypt), /konto/registrieren|anmelden|abmelden, /konto (Bestellhistorie+Adressbuch), Tabelle customer_addresses, Checkout-Vorbefuellung + orders.customer_id-Zuordnung; Gast-Checkout bleibt
- feature_reviews: Tabelle reviews (1-5, Moderation), /api/review (approved=0), PDP-Anzeige Durchschnitt+Reviews + aggregateRating-JSON-LD, Admin /bewertungen (Freigeben/Verbergen/Loeschen) + Nav-Zaehler
- feature_abandoned_cart: Tabelle abandoned_carts, /api/cart-capture beim Checkout-Start, /api/cron/abandoned (CRON_TOKEN) sendet Erinnerungsmail (Mailer/Log) + reminded=1, recovered=1 bei Bestellung; Status in Einstellungen
- Gating: Flag aus => Storefront-Elemente weg, Routen 302/404, Admin-Nav-Punkt entfaellt; KEIN 'in Vorbereitung' mehr
- API/MCP: reviews CRUD + abandoned_carts (read) in admin-api + ai-admin.txt + MCP-Tools; Manifest v2.3
- README + .env.example (CRON_TOKEN, ABANDONED_AFTER_MINUTES); 16 neue Unit-Tests (Suche/Review-Avg/Kunden/Abandoned)
2026-06-18 07:27:34 +00:00

98 lines
8.6 KiB
JavaScript

#!/usr/bin/env node
// hd-commerce MCP-Server (stdio).
// Spricht die token-gesicherte Admin-API (/api/admin) an.
// ENV: HDC_BASE_URL (z.B. https://shop.example.com), HDC_API_TOKEN.
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
const BASE = (process.env.HDC_BASE_URL || 'http://localhost:4321').replace(/\/+$/, '');
const TOKEN = process.env.HDC_API_TOKEN || '';
async function api(method, path, body) {
const res = await fetch(BASE + path, {
method,
headers: { 'Authorization': 'Bearer ' + TOKEN, 'Content-Type': 'application/json' },
body: body !== undefined ? JSON.stringify(body) : undefined,
});
const text = await res.text();
let data; try { data = JSON.parse(text); } catch { data = text; }
if (!res.ok) throw new Error('API ' + res.status + ': ' + (data && data.error ? data.error : text));
return data;
}
const TOOLS = [
{ name: 'list_products', description: 'Alle Produkte auflisten.', inputSchema: { type: 'object', properties: {} } },
{ name: 'get_product', description: 'Ein Produkt per ID oder Slug holen.', inputSchema: { type: 'object', properties: { id: { type: 'string', description: 'Produkt-ID' } }, required: ['id'] } },
{ name: 'upsert_product', description: 'Produkt anlegen/aktualisieren. Mit id oder slug => Update, sonst Create. Preis in Cent (priceCents).', inputSchema: { type: 'object', properties: { product: { type: 'object', description: 'Produktfelder: name, slug, priceCents, category, desc, cardImage, images[], stock, featured, badge, sizes[], features[]' } }, required: ['product'] } },
{ name: 'delete_product', description: 'Produkt löschen (per ID).', inputSchema: { type: 'object', properties: { id: { type: 'string' } }, required: ['id'] } },
{ name: 'list_pages', description: 'Alle Seiten auflisten.', inputSchema: { type: 'object', properties: {} } },
{ name: 'get_page', description: 'Seite per ID oder Slug (inkl. blocks).', inputSchema: { type: 'object', properties: { id: { type: 'string' } }, required: ['id'] } },
{ name: 'create_page', description: 'Neue Seite anlegen (slug, title, body, type, blocks[]).', inputSchema: { type: 'object', properties: { page: { type: 'object' } }, required: ['page'] } },
{ name: 'update_page_blocks', description: 'Block-Array einer Seite setzen (Visual-Builder-Struktur).', inputSchema: { type: 'object', properties: { id: { type: 'string' }, blocks: { type: 'array' } }, required: ['id', 'blocks'] } },
{ name: 'list_orders', description: 'Bestellungen auflisten (nur lesen).', inputSchema: { type: 'object', properties: {} } },
{ name: 'list_slides', description: 'Slider-Slides auflisten.', inputSchema: { type: 'object', properties: {} } },
{ name: 'upsert_slide', description: 'Slide anlegen/aktualisieren.', inputSchema: { type: 'object', properties: { slide: { type: 'object' } }, required: ['slide'] } },
{ name: 'list_discounts', description: 'Alle Rabatte/Gutscheine auflisten.', inputSchema: { type: 'object', properties: {} } },
{ name: 'upsert_discount', description: 'Rabatt anlegen/aktualisieren. Mit id oder code => Update, sonst Create. type: percent|fixed|freeshipping; value bei percent 1-100, bei fixed in Cent.', inputSchema: { type: 'object', properties: { discount: { type: 'object', description: 'Felder: code, title, type, value, min_order_cents, starts_at, expires_at, max_uses, max_per_customer, active, secret, auto' } }, required: ['discount'] } },
{ name: 'delete_discount', description: 'Rabatt löschen (per ID).', inputSchema: { type: 'object', properties: { id: { type: 'string' } }, required: ['id'] } },
{ name: 'list_shipping', description: 'Versandzonen auflisten.', inputSchema: { type: 'object', properties: {} } },
{ name: 'upsert_shipping', description: 'Versandzone anlegen/aktualisieren (mit id => Update). Felder: name, countries (CSV ISO / EU), price_cents, free_over_cents, delivery_days, sort, active.', inputSchema: { type: 'object', properties: { zone: { type: 'object' } }, required: ['zone'] } },
{ name: 'delete_shipping', description: 'Versandzone löschen (per ID).', inputSchema: { type: 'object', properties: { id: { type: 'string' } }, required: ['id'] } },
{ name: 'get_settings', description: 'Shop-Einstellungen (Key/Value) holen.', inputSchema: { type: 'object', properties: {} } },
{ name: 'update_settings', description: 'Shop-Einstellungen aktualisieren (Key/Value-Map, z.B. shop_name, brand_accent).', inputSchema: { type: 'object', properties: { settings: { type: 'object' } }, required: ['settings'] } },
{ name: 'list_reviews', description: 'Produktbewertungen auflisten (inkl. approved-Status).', inputSchema: { type: 'object', properties: {} } },
{ name: 'moderate_review', description: 'Bewertung moderieren: { id, approved: true|false }. Ohne id: neue Bewertung { product_slug, name, rating(1-5), text }.', inputSchema: { type: 'object', properties: { review: { type: 'object' } }, required: ['review'] } },
{ name: 'delete_review', description: 'Bewertung löschen (per ID).', inputSchema: { type: 'object', properties: { id: { type: 'string' } }, required: ['id'] } },
{ name: 'list_abandoned_carts', description: 'Abgebrochene Warenkörbe auflisten (nur lesen).', inputSchema: { type: 'object', properties: {} } },
{ name: 'get_manifest', description: 'API-Manifest (alle Ressourcen, Felder, Block-Typen).', inputSchema: { type: 'object', properties: {} } },
];
const server = new Server({ name: 'hd-commerce', version: '2.3.0' }, { capabilities: { tools: {} } });
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
server.setRequestHandler(CallToolRequestSchema, async (req) => {
const { name, arguments: a = {} } = req.params;
let out;
try {
switch (name) {
case 'list_products': out = await api('GET', '/api/admin/products'); break;
case 'get_product': out = await api('GET', '/api/admin/products/' + encodeURIComponent(a.id)); break;
case 'upsert_product': out = await api('POST', '/api/admin/products', a.product); break;
case 'delete_product': out = await api('DELETE', '/api/admin/products/' + encodeURIComponent(a.id)); break;
case 'list_pages': out = await api('GET', '/api/admin/pages'); break;
case 'get_page': out = await api('GET', '/api/admin/pages/' + encodeURIComponent(a.id)); break;
case 'create_page': out = await api('POST', '/api/admin/pages', a.page); break;
case 'update_page_blocks': out = await api('POST', '/api/admin/pages/' + encodeURIComponent(a.id) + '/blocks', { blocks: a.blocks }); break;
case 'list_orders': out = await api('GET', '/api/admin/orders'); break;
case 'list_slides': out = await api('GET', '/api/admin/slides'); break;
case 'upsert_slide': out = await api('POST', '/api/admin/slides', a.slide); break;
case 'list_discounts': out = await api('GET', '/api/admin/discounts'); break;
case 'upsert_discount': out = await api('POST', '/api/admin/discounts', a.discount); break;
case 'delete_discount': out = await api('DELETE', '/api/admin/discounts/' + encodeURIComponent(a.id)); break;
case 'list_shipping': out = await api('GET', '/api/admin/shipping_zones'); break;
case 'upsert_shipping': out = await api('POST', '/api/admin/shipping_zones', a.zone); break;
case 'delete_shipping': out = await api('DELETE', '/api/admin/shipping_zones/' + encodeURIComponent(a.id)); break;
case 'get_settings': out = await api('GET', '/api/admin/settings'); break;
case 'update_settings': out = await api('POST', '/api/admin/settings', a.settings); break;
case 'list_reviews': out = await api('GET', '/api/admin/reviews'); break;
case 'moderate_review': out = await api('POST', '/api/admin/reviews', a.review); break;
case 'delete_review': out = await api('DELETE', '/api/admin/reviews/' + encodeURIComponent(a.id)); break;
case 'list_abandoned_carts': out = await api('GET', '/api/admin/abandoned_carts'); break;
case 'get_manifest': out = await api('GET', '/api/admin'); break;
default: throw new Error('Unbekanntes Tool: ' + name);
}
return { content: [{ type: 'text', text: JSON.stringify(out, null, 2) }] };
} catch (e) {
return { content: [{ type: 'text', text: 'Fehler: ' + (e && e.message || e) }], isError: true };
}
});
async function main() {
if (!TOKEN) console.error('[hd-commerce-mcp] Warnung: HDC_API_TOKEN ist nicht gesetzt.');
const transport = new StdioServerTransport();
await server.connect(transport);
console.error('[hd-commerce-mcp] bereit. Base: ' + BASE);
}
main().catch((e) => { console.error(e); process.exit(1); });