From 481da014bc8c9ba3c4598f2d924006eadefa28c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathias=20Gla=CC=88ser?= Date: Sat, 19 Sep 2026 21:13:43 +0200 Subject: [PATCH] =?UTF-8?q?MCP-API=20an=20GUI-Felder=20und=20Durchf=C3=BCh?= =?UTF-8?q?rungsdaten=20angleichen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../market_management_hermes_mcp_api.md | 5 + modules/market_management/mcp.php | 142 ++++++++++++++++-- 2 files changed, 133 insertions(+), 14 deletions(-) diff --git a/docs/modules/market_management/konzepte/market_management_hermes_mcp_api.md b/docs/modules/market_management/konzepte/market_management_hermes_mcp_api.md index baaed8b..c8b35ed 100644 --- a/docs/modules/market_management/konzepte/market_management_hermes_mcp_api.md +++ b/docs/modules/market_management/konzepte/market_management_hermes_mcp_api.md @@ -78,6 +78,7 @@ Das Ergebnis steht bei `tools/call` als JSON im Textinhalt des ersten - `market_management.get_market` - erforderlich: `market_id` - optional: `include_archived` + - liefert Durchführungen inklusive Tagen, Kosten und berechneten Verkaufswerten - `market_management.create_market` - erforderlich: `name`, `category_id` - optional: `website_url` als HTTP(S)-URL der Veranstaltung @@ -109,8 +110,12 @@ Kategorien anlegen oder ändern. - weitere Durchführungsfelder sind optional und werden typisiert validiert - `market_management.get_occurrence` - erforderlich: `occurrence_id` + - liefert zusätzlich die Marktdefinition, Durchführungstage, Kosten und + berechnete Tages- und Gesamterlöse - `market_management.update_occurrence` - erforderlich: `occurrence_id` + - änderbar: sämtliche GUI-Durchführungsfelder einschließlich Veranstalter, + Kontaktperson, Kontakt-E-Mail und Kontakt-Telefon - `market_management.archive_occurrence` - erforderlich: `occurrence_id` - `market_management.reactivate_occurrence` diff --git a/modules/market_management/mcp.php b/modules/market_management/mcp.php index 3454f52..4d00543 100644 --- a/modules/market_management/mcp.php +++ b/modules/market_management/mcp.php @@ -30,24 +30,63 @@ function market_management_mcp_authorize(array $env): string function market_management_mcp_tools(): array { + $occurrenceProperties = [ + 'status' => ['type' => 'string'], 'place' => ['type' => ['string', 'null']], 'address' => ['type' => ['string', 'null']], + 'municipality_reference_id' => ['type' => ['integer', 'null']], 'notes' => ['type' => ['string', 'null']], + 'registration_deadline' => ['type' => ['string', 'null'], 'format' => 'date'], + 'request_or_registration_date' => ['type' => ['string', 'null'], 'format' => 'date'], + 'confirmation_date' => ['type' => ['string', 'null'], 'format' => 'date'], + 'payment_deadline' => ['type' => ['string', 'null'], 'format' => 'date'], + 'payment_status' => ['type' => ['string', 'null'], 'enum' => ['paid', 'unpaid']], + 'payment_date' => ['type' => ['string', 'null'], 'format' => 'date'], 'paid_amount' => ['type' => ['number', 'null']], + 'payment_method_code' => ['type' => ['string', 'null']], 'stand_size' => ['type' => ['string', 'null']], + 'table_available' => ['type' => ['boolean', 'null']], 'table_rentable' => ['type' => ['boolean', 'null']], + 'own_table_required' => ['type' => ['boolean', 'null']], 'shelter_available' => ['type' => ['boolean', 'null']], + 'power_available' => ['type' => ['boolean', 'null']], 'water_available' => ['type' => ['boolean', 'null']], + 'lighting_available' => ['type' => ['boolean', 'null']], 'other_infrastructure' => ['type' => ['string', 'null']], + 'own_stand_requirements' => ['type' => ['string', 'null']], 'travel_address' => ['type' => ['string', 'null']], + 'travel_distance_km' => ['type' => ['number', 'null']], 'travel_duration_minutes' => ['type' => ['integer', 'null']], + 'parking_options' => ['type' => ['string', 'null']], 'delivery_access' => ['type' => ['string', 'null']], + 'direct_unload_possible' => ['type' => ['boolean', 'null']], 'storage_options' => ['type' => ['string', 'null']], + 'travel_notes' => ['type' => ['string', 'null']], 'cancellation_terms' => ['type' => ['string', 'null']], + 'revenue_share_terms' => ['type' => ['string', 'null']], 'other_participation_conditions' => ['type' => ['string', 'null']], + 'contract_folder_url' => ['type' => ['string', 'null'], 'format' => 'uri'], + 'organization_contact_id' => ['type' => ['integer', 'null']], 'contact_person_id' => ['type' => ['integer', 'null']], + 'contact_email' => ['type' => ['string', 'null'], 'format' => 'email'], 'contact_phone' => ['type' => ['string', 'null']], + ]; + $dayProperties = [ + 'date' => ['type' => 'string', 'format' => 'date'], 'observations' => ['type' => ['string', 'null']], + 'stand_start_time' => ['type' => ['string', 'null'], 'format' => 'time'], 'stand_end_time' => ['type' => ['string', 'null'], 'format' => 'time'], + 'setup_start_time' => ['type' => ['string', 'null'], 'format' => 'time'], 'setup_end_time' => ['type' => ['string', 'null'], 'format' => 'time'], + 'delivery_start_time' => ['type' => ['string', 'null'], 'format' => 'time'], 'delivery_end_time' => ['type' => ['string', 'null'], 'format' => 'time'], + 'inherit_from_first_day' => ['type' => ['boolean', 'null']], + ]; + $searchProperties = [ + 'search' => ['type' => 'string'], 'market_definition_id' => ['type' => 'integer'], 'category_id' => ['type' => 'integer'], + 'status' => ['type' => 'string'], 'place' => ['type' => 'string'], 'address' => ['type' => 'string'], + 'municipality_reference_id' => ['type' => 'integer'], 'date_from' => ['type' => 'string', 'format' => 'date'], + 'date_to' => ['type' => 'string', 'format' => 'date'], 'include_archived' => ['type' => 'boolean'], + 'sort' => ['type' => 'string'], 'sort_direction' => ['type' => 'string', 'enum' => ['asc', 'desc']], + 'limit' => ['type' => 'integer', 'minimum' => 1, 'maximum' => 100], 'offset' => ['type' => 'integer', 'minimum' => 0], + ]; return [ - ['name' => 'market_management.search_markets', 'description' => 'Märkte suchen', 'inputSchema' => ['type' => 'object']], - ['name' => 'market_management.search_occurrences', 'description' => 'Durchführungen suchen und auflisten', 'inputSchema' => ['type' => 'object']], - ['name' => 'market_management.get_market', 'description' => 'Markt mit Durchführungen lesen', 'inputSchema' => ['type' => 'object', 'required' => ['market_id']]], + ['name' => 'market_management.search_markets', 'description' => 'Märkte suchen', 'inputSchema' => ['type' => 'object', 'properties' => $searchProperties]], + ['name' => 'market_management.search_occurrences', 'description' => 'Durchführungen suchen und auflisten', 'inputSchema' => ['type' => 'object', 'properties' => $searchProperties]], + ['name' => 'market_management.get_market', 'description' => 'Markt mit Durchführungen lesen', 'inputSchema' => ['type' => 'object', 'required' => ['market_id'], 'properties' => ['market_id' => ['type' => 'integer'], 'include_archived' => ['type' => 'boolean']]]], ['name' => 'market_management.create_market', 'description' => 'Markt mit bestehender Kategorie und optionaler Website-URL anlegen', 'inputSchema' => ['type' => 'object', 'required' => ['name', 'category_id'], 'properties' => ['name' => ['type' => 'string'], 'category_id' => ['type' => 'integer'], 'website_url' => ['type' => ['string', 'null'], 'format' => 'uri']]]], ['name' => 'market_management.update_market', 'description' => 'Markt inklusive Website-URL ändern', 'inputSchema' => ['type' => 'object', 'required' => ['market_id'], 'properties' => ['market_id' => ['type' => 'integer'], 'name' => ['type' => 'string'], 'category_id' => ['type' => 'integer'], 'website_url' => ['type' => ['string', 'null'], 'format' => 'uri']]]], ['name' => 'market_management.archive_market', 'description' => 'Markt archivieren', 'inputSchema' => ['type' => 'object', 'required' => ['market_id']]], ['name' => 'market_management.reactivate_market', 'description' => 'Markt reaktivieren', 'inputSchema' => ['type' => 'object', 'required' => ['market_id']]], ['name' => 'market_management.list_categories', 'description' => 'Bestehende Kategorien lesen', 'inputSchema' => ['type' => 'object']], ['name' => 'market_management.list_reference_data', 'description' => 'Kategorien, Kantone und Gemeinden lesen', 'inputSchema' => ['type' => 'object']], - ['name' => 'market_management.get_overview', 'description' => 'Marktübersicht mit serverseitigen Filtern lesen', 'inputSchema' => ['type' => 'object']], - ['name' => 'market_management.create_occurrence', 'description' => 'Durchführung mit mindestens einem Tag anlegen', 'inputSchema' => ['type' => 'object', 'required' => ['market_definition_id', 'days']]], - ['name' => 'market_management.get_occurrence', 'description' => 'Durchführung mit Tagen lesen', 'inputSchema' => ['type' => 'object', 'required' => ['occurrence_id']]], - ['name' => 'market_management.update_occurrence', 'description' => 'Durchführung ändern', 'inputSchema' => ['type' => 'object', 'required' => ['occurrence_id']]], + ['name' => 'market_management.get_overview', 'description' => 'Marktübersicht mit serverseitigen Filtern lesen', 'inputSchema' => ['type' => 'object', 'properties' => $searchProperties]], + ['name' => 'market_management.create_occurrence', 'description' => 'Durchführung mit mindestens einem Tag anlegen', 'inputSchema' => ['type' => 'object', 'required' => ['market_definition_id', 'days'], 'properties' => ['market_definition_id' => ['type' => 'integer'], 'status' => ['type' => 'string'], 'days' => ['type' => 'array', 'minItems' => 1, 'items' => ['type' => 'object', 'required' => ['date'], 'properties' => $dayProperties]] + $occurrenceProperties]]], + ['name' => 'market_management.get_occurrence', 'description' => 'Durchführung mit Markt, Tagen und berechneten Verkaufsdaten lesen', 'inputSchema' => ['type' => 'object', 'required' => ['occurrence_id'], 'properties' => ['occurrence_id' => ['type' => 'integer'], 'include_archived' => ['type' => 'boolean']]]], + ['name' => 'market_management.update_occurrence', 'description' => 'Durchführung ändern', 'inputSchema' => ['type' => 'object', 'required' => ['occurrence_id'], 'properties' => ['occurrence_id' => ['type' => 'integer']] + $occurrenceProperties]], ['name' => 'market_management.archive_occurrence', 'description' => 'Durchführung archivieren', 'inputSchema' => ['type' => 'object', 'required' => ['occurrence_id']]], ['name' => 'market_management.reactivate_occurrence', 'description' => 'Durchführung reaktivieren', 'inputSchema' => ['type' => 'object', 'required' => ['occurrence_id']]], - ['name' => 'market_management.create_day', 'description' => 'Durchführungstag anlegen', 'inputSchema' => ['type' => 'object', 'required' => ['occurrence_id', 'date']]], - ['name' => 'market_management.update_day', 'description' => 'Durchführungstag ändern', 'inputSchema' => ['type' => 'object', 'required' => ['day_id']]], + ['name' => 'market_management.create_day', 'description' => 'Durchführungstag anlegen', 'inputSchema' => ['type' => 'object', 'required' => ['occurrence_id', 'date'], 'properties' => ['occurrence_id' => ['type' => 'integer']] + $dayProperties]], + ['name' => 'market_management.update_day', 'description' => 'Durchführungstag ändern', 'inputSchema' => ['type' => 'object', 'required' => ['day_id'], 'properties' => ['day_id' => ['type' => 'integer']] + $dayProperties]], ['name' => 'market_management.delete_day', 'description' => 'Durchführungstag entfernen', 'inputSchema' => ['type' => 'object', 'required' => ['day_id']]], ['name' => 'market_management.set_organizer', 'description' => 'Veranstalter einer Durchführung setzen', 'inputSchema' => ['type' => 'object', 'required' => ['occurrence_id', 'organization_contact_id', 'contact_person_id']]], ['name' => 'market_management.delete_organizer', 'description' => 'Veranstalter einer Durchführung entfernen', 'inputSchema' => ['type' => 'object', 'required' => ['occurrence_id']]], @@ -146,16 +185,91 @@ function market_management_mcp_call_idempotent(PDO $pdo, string $userSubject, st } } +function market_management_mcp_get_occurrence(PDO $pdo, int $occurrenceId, bool $includeArchived = false): array +{ + $occurrence = market_management_get_occurrence($pdo, $occurrenceId, $includeArchived); + if ($occurrence === null) { + throw new RuntimeException('Durchführung nicht gefunden.'); + } + $market = market_management_get_market($pdo, (int) $occurrence['market_definition_id'], $includeArchived); + if ($market === null) { + throw new RuntimeException('Markt nicht gefunden.'); + } + unset($market['occurrences']); + $occurrence['market'] = $market; + return $occurrence; +} + +function market_management_mcp_get_market(PDO $pdo, int $marketId, bool $includeArchived = false): array +{ + $market = market_management_get_market($pdo, $marketId, $includeArchived); + if ($market === null) { + throw new RuntimeException('Markt nicht gefunden.'); + } + foreach ($market['occurrences'] as $index => $occurrence) { + $enriched = market_management_get_occurrence($pdo, (int) $occurrence['id'], $includeArchived); + if ($enriched !== null) { + $market['occurrences'][$index] = $enriched; + } + } + return $market; +} + +function market_management_mcp_update_occurrence(PDO $pdo, int $occurrenceId, array $input): array +{ + $contactFields = []; + foreach (['contact_email', 'contact_phone'] as $field) { + if (array_key_exists($field, $input)) { + $contactFields[substr($field, 8)] = $input[$field]; + unset($input[$field]); + } + } + $organizerFields = []; + foreach (['organization_contact_id', 'contact_person_id'] as $field) { + if (array_key_exists($field, $input)) { + $organizerFields[$field] = $input[$field]; + unset($input[$field]); + } + } + $existing = null; + if ($contactFields !== [] || $organizerFields !== []) { + $existing = market_management_get_occurrence($pdo, $occurrenceId, true); + if ($existing === null) { + throw new RuntimeException('Durchführung nicht gefunden.'); + } + } + if ($organizerFields !== []) { + $organizer = $existing['organizer'] ?? []; + $organizerFields['occurrence_id'] = $occurrenceId; + $organizerFields['organization_contact_id'] ??= $organizer['organization_contact_id'] ?? null; + $organizerFields['contact_person_id'] ??= $organizer['contact_person_id'] ?? null; + market_management_set_organizer($pdo, $organizerFields); + } + if ($contactFields !== []) { + $contactId = (int) ($organizerFields['contact_person_id'] ?? $existing['contact_person_id'] ?? 0); + $hasContactValue = array_reduce($contactFields, static fn (bool $carry, mixed $value): bool => $carry || trim((string) $value) !== '', false); + if ($contactId > 0 && ($hasContactValue || array_key_exists('contact_person_id', $organizerFields))) { + update_contact_details($pdo, $contactId, $contactFields); + } elseif ($hasContactValue) { + throw new InvalidArgumentException('Für E-Mail oder Telefon muss eine Kontaktperson ausgewählt sein.'); + } + } + if ($input !== []) { + market_management_update_occurrence($pdo, $occurrenceId, $input); + } + return market_management_mcp_get_occurrence($pdo, $occurrenceId, true); +} + function market_management_mcp_call(PDO $pdo, string $tool, array $arguments): array { return match ($tool) { 'market_management.search_markets' => market_management_search_markets($pdo, $arguments), 'market_management.search_occurrences' => market_management_search_occurrences($pdo, $arguments), - 'market_management.get_market' => market_management_get_market( + 'market_management.get_market' => market_management_mcp_get_market( $pdo, market_management_require_positive_id($arguments['market_id'] ?? null, 'market_id'), (bool) ($arguments['include_archived'] ?? false) - ) ?? throw new RuntimeException('Markt nicht gefunden.'), + ), 'market_management.create_market' => market_management_create_market($pdo, $arguments), 'market_management.update_market' => market_management_update_market( $pdo, @@ -179,12 +293,12 @@ function market_management_mcp_call(PDO $pdo, string $tool, array $arguments): a 'occurrences' => market_management_search_occurrences($pdo, $arguments), ], 'market_management.create_occurrence' => market_management_create_occurrence($pdo, $arguments), - 'market_management.get_occurrence' => market_management_get_occurrence( + 'market_management.get_occurrence' => market_management_mcp_get_occurrence( $pdo, market_management_require_positive_id($arguments['occurrence_id'] ?? null, 'occurrence_id'), (bool) ($arguments['include_archived'] ?? false) - ) ?? throw new RuntimeException('Durchführung nicht gefunden.'), - 'market_management.update_occurrence' => market_management_update_occurrence( + ), + 'market_management.update_occurrence' => market_management_mcp_update_occurrence( $pdo, market_management_require_positive_id($arguments['occurrence_id'] ?? null, 'occurrence_id'), $arguments