From d884793cb0b23caaa90038e4abff6457d4c3a75e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathias=20Gla=CC=88ser?= Date: Sun, 18 Oct 2026 09:41:53 +0200 Subject: [PATCH] Expose MCP idempotency keys --- .../market_management_hermes_mcp_api.md | 3 +- modules/market_management/mcp.php | 29 ++++++++++--------- .../market_management_mcp_tools_list_test.php | 15 ++++++++++ 3 files changed, 32 insertions(+), 15 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 25cbcf9..1434041 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 @@ -328,7 +328,8 @@ Weitere mögliche Tagesfelder sind `stand_start_time`, `stand_end_time`, `sku`, `product_name`, `quantity`, `gross_revenue` und `unit_price`. - Markt und Marktdurchführung werden unabhängig archiviert. - Alle Schreibvorgänge werden serverseitig validiert. -- Jeder schreibende MCP-Aufruf benötigt `idempotency_key`; optional kann +- Jeder schreibende MCP-Aufruf benötigt `idempotency_key`; das Feld ist in den + Schemas aller Schreibwerkzeuge als Pflichtfeld deklariert. Optional kann `client_id` mitgegeben werden. Wiederholungen liefern dasselbe Ergebnis. - Die gelieferten Verkaufspositionen stammen ausschließlich aus OTC-Verkäufen, die einem konkreten Markttag zugeordnet sind. diff --git a/modules/market_management/mcp.php b/modules/market_management/mcp.php index b22b408..7a5bd7d 100644 --- a/modules/market_management/mcp.php +++ b/modules/market_management/mcp.php @@ -30,6 +30,7 @@ function market_management_mcp_authorize(array $env): string function market_management_mcp_tools(): array { + $idempotencyProperty = ['idempotency_key' => ['type' => 'string', 'minLength' => 1]]; $occurrenceProperties = [ 'status' => ['type' => 'string'], 'place' => ['type' => ['string', 'null']], 'address' => ['type' => ['string', 'null']], 'municipality_reference_id' => ['type' => ['integer', 'null']], 'notes' => ['type' => ['string', 'null']], @@ -75,26 +76,26 @@ function market_management_mcp_tools(): array ['name' => 'market_management.search_markets', 'description' => 'Märkte suchen', 'inputSchema' => ['type' => 'object', 'properties' => $searchProperties]], ['name' => 'market_management.search_occurrences', 'description' => 'Marktdurchführungen suchen und auflisten', 'inputSchema' => ['type' => 'object', 'properties' => $searchProperties]], ['name' => 'market_management.get_market', 'description' => 'Markt mit Marktdurchfü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 verpflichtendem Veranstalter, Kategorie und optionaler Website-URL anlegen', 'inputSchema' => ['type' => 'object', 'required' => ['name', 'category_id', 'organizer_id'], 'properties' => ['name' => ['type' => 'string'], 'category_id' => ['type' => 'integer'], 'organizer_id' => ['type' => 'integer'], 'website_url' => ['type' => ['string', 'null']]]]], - ['name' => 'market_management.update_market', 'description' => 'Markt inklusive Veranstalter und Website-URL ändern', 'inputSchema' => ['type' => 'object', 'required' => ['market_id'], 'properties' => ['market_id' => ['type' => 'integer'], 'name' => ['type' => 'string'], 'category_id' => ['type' => 'integer'], 'organizer_id' => ['type' => 'integer'], 'website_url' => ['type' => ['string', 'null']]]]], - ['name' => 'market_management.archive_market', 'description' => 'Markt archivieren', 'inputSchema' => ['type' => 'object', 'required' => ['market_id'], 'properties' => ['market_id' => ['type' => 'integer']]]], - ['name' => 'market_management.reactivate_market', 'description' => 'Markt reaktivieren', 'inputSchema' => ['type' => 'object', 'required' => ['market_id'], 'properties' => ['market_id' => ['type' => 'integer']]]], + ['name' => 'market_management.create_market', 'description' => 'Markt mit verpflichtendem Veranstalter, Kategorie und optionaler Website-URL anlegen', 'inputSchema' => ['type' => 'object', 'required' => ['name', 'category_id', 'organizer_id', 'idempotency_key'], 'properties' => $idempotencyProperty + ['name' => ['type' => 'string'], 'category_id' => ['type' => 'integer'], 'organizer_id' => ['type' => 'integer'], 'website_url' => ['type' => ['string', 'null']]]]], + ['name' => 'market_management.update_market', 'description' => 'Markt inklusive Veranstalter und Website-URL ändern', 'inputSchema' => ['type' => 'object', 'required' => ['market_id', 'idempotency_key'], 'properties' => $idempotencyProperty + ['market_id' => ['type' => 'integer'], 'name' => ['type' => 'string'], 'category_id' => ['type' => 'integer'], 'organizer_id' => ['type' => 'integer'], 'website_url' => ['type' => ['string', 'null']]]]], + ['name' => 'market_management.archive_market', 'description' => 'Markt archivieren', 'inputSchema' => ['type' => 'object', 'required' => ['market_id', 'idempotency_key'], 'properties' => $idempotencyProperty + ['market_id' => ['type' => 'integer']]]], + ['name' => 'market_management.reactivate_market', 'description' => 'Markt reaktivieren', 'inputSchema' => ['type' => 'object', 'required' => ['market_id', 'idempotency_key'], 'properties' => $idempotencyProperty + ['market_id' => ['type' => 'integer']]]], ['name' => 'market_management.list_categories', 'description' => 'Bestehende Kategorien lesen', 'inputSchema' => ['type' => 'object', 'properties' => (object) []]], ['name' => 'market_management.list_market_organizers', 'description' => 'Aktive Veranstalter lesen', 'inputSchema' => ['type' => 'object', 'properties' => (object) []]], - ['name' => 'market_management.create_market_organizer', 'description' => 'Veranstalter mit optionalen Kontaktdaten anlegen', 'inputSchema' => ['type' => 'object', 'required' => ['name'], 'properties' => ['name' => ['type' => 'string'], 'email' => ['type' => ['string', 'null'], 'format' => 'email'], 'phone' => ['type' => ['string', 'null']]]]], + ['name' => 'market_management.create_market_organizer', 'description' => 'Veranstalter mit optionalen Kontaktdaten anlegen', 'inputSchema' => ['type' => 'object', 'required' => ['name', 'idempotency_key'], 'properties' => $idempotencyProperty + ['name' => ['type' => 'string'], 'email' => ['type' => ['string', 'null'], 'format' => 'email'], 'phone' => ['type' => ['string', 'null']]]]], ['name' => 'market_management.list_reference_data', 'description' => 'Kategorien, Kantone und Gemeinden lesen', 'inputSchema' => ['type' => 'object', 'properties' => (object) []]], - ['name' => 'market_management.create_municipality', 'description' => 'Lokale Gemeinde im Markt-Gemeinderegister anlegen', 'inputSchema' => ['type' => 'object', 'required' => ['name'], 'properties' => ['name' => ['type' => 'string']]]], + ['name' => 'market_management.create_municipality', 'description' => 'Lokale Gemeinde im Markt-Gemeinderegister anlegen', 'inputSchema' => ['type' => 'object', 'required' => ['name', 'idempotency_key'], 'properties' => $idempotencyProperty + ['name' => ['type' => 'string']]]], ['name' => 'market_management.get_overview', 'description' => 'Marktübersicht mit serverseitigen Filtern lesen', 'inputSchema' => ['type' => 'object', 'properties' => $searchProperties]], - ['name' => 'market_management.create_occurrence', 'description' => 'Marktdurchführung mit mindestens einem Markttag anlegen', 'inputSchema' => ['type' => 'object', 'required' => ['market_definition_id', 'days'], 'properties' => ['market_definition_id' => ['type' => 'integer'], 'status' => ['type' => 'string']] + $occurrenceProperties + ['days' => ['type' => 'array', 'minItems' => 1, 'items' => ['type' => 'object', 'required' => ['date'], 'properties' => $dayProperties]]]]], + ['name' => 'market_management.create_occurrence', 'description' => 'Marktdurchführung mit mindestens einem Markttag anlegen', 'inputSchema' => ['type' => 'object', 'required' => ['market_definition_id', 'days', 'idempotency_key'], 'properties' => $idempotencyProperty + ['market_definition_id' => ['type' => 'integer'], 'status' => ['type' => 'string']] + $occurrenceProperties + ['days' => ['type' => 'array', 'minItems' => 1, 'items' => ['type' => 'object', 'required' => ['date'], 'properties' => $dayProperties]]]]], ['name' => 'market_management.get_occurrence', 'description' => 'Marktdurchführung mit Markt, Markttagen und berechneten Verkaufsdaten lesen', 'inputSchema' => ['type' => 'object', 'required' => ['occurrence_id'], 'properties' => ['occurrence_id' => ['type' => 'integer'], 'include_archived' => ['type' => 'boolean']]]], ['name' => 'market_management.get_occurrence_checklist', 'description' => 'Checkliste einer Marktdurchführung lesen', 'inputSchema' => ['type' => 'object', 'required' => ['occurrence_id'], 'properties' => ['occurrence_id' => ['type' => 'integer'], 'include_archived' => ['type' => 'boolean']]]], - ['name' => 'market_management.update_occurrence_checklist', 'description' => 'Checkliste einer Marktdurchführung vollständig ersetzen', 'inputSchema' => ['type' => 'object', 'required' => ['occurrence_id', 'items'], 'properties' => ['occurrence_id' => ['type' => 'integer'], 'comment' => ['type' => ['string', 'null']], 'items' => ['type' => 'array', 'items' => ['type' => 'object', 'required' => ['chapter_key', 'label', 'is_completed'], 'properties' => ['chapter_key' => ['type' => 'string', 'enum' => ['products_sales', 'decoration', 'stand_equipment', 'technology']], 'label' => ['type' => 'string'], 'is_completed' => ['type' => 'boolean']]]]]]], - ['name' => 'market_management.update_occurrence', 'description' => 'Marktdurchführung ändern', 'inputSchema' => ['type' => 'object', 'required' => ['occurrence_id'], 'properties' => ['occurrence_id' => ['type' => 'integer']] + $occurrenceProperties]], - ['name' => 'market_management.archive_occurrence', 'description' => 'Marktdurchführung archivieren', 'inputSchema' => ['type' => 'object', 'required' => ['occurrence_id'], 'properties' => ['occurrence_id' => ['type' => 'integer']]]], - ['name' => 'market_management.reactivate_occurrence', 'description' => 'Marktdurchführung reaktivieren', 'inputSchema' => ['type' => 'object', 'required' => ['occurrence_id'], 'properties' => ['occurrence_id' => ['type' => 'integer']]]], - ['name' => 'market_management.create_day', 'description' => 'Markttag anlegen', 'inputSchema' => ['type' => 'object', 'required' => ['occurrence_id', 'date'], 'properties' => ['occurrence_id' => ['type' => 'integer']] + $dayProperties]], - ['name' => 'market_management.update_day', 'description' => 'Markttag ändern', 'inputSchema' => ['type' => 'object', 'required' => ['day_id'], 'properties' => ['day_id' => ['type' => 'integer']] + $dayProperties]], - ['name' => 'market_management.delete_day', 'description' => 'Markttag entfernen', 'inputSchema' => ['type' => 'object', 'required' => ['day_id'], 'properties' => ['day_id' => ['type' => 'integer']]]], + ['name' => 'market_management.update_occurrence_checklist', 'description' => 'Checkliste einer Marktdurchführung vollständig ersetzen', 'inputSchema' => ['type' => 'object', 'required' => ['occurrence_id', 'items', 'idempotency_key'], 'properties' => $idempotencyProperty + ['occurrence_id' => ['type' => 'integer'], 'comment' => ['type' => ['string', 'null']], 'items' => ['type' => 'array', 'items' => ['type' => 'object', 'required' => ['chapter_key', 'label', 'is_completed'], 'properties' => ['chapter_key' => ['type' => 'string', 'enum' => ['products_sales', 'decoration', 'stand_equipment', 'technology']], 'label' => ['type' => 'string'], 'is_completed' => ['type' => 'boolean']]]]]]], + ['name' => 'market_management.update_occurrence', 'description' => 'Marktdurchführung ändern', 'inputSchema' => ['type' => 'object', 'required' => ['occurrence_id', 'idempotency_key'], 'properties' => $idempotencyProperty + ['occurrence_id' => ['type' => 'integer']] + $occurrenceProperties]], + ['name' => 'market_management.archive_occurrence', 'description' => 'Marktdurchführung archivieren', 'inputSchema' => ['type' => 'object', 'required' => ['occurrence_id', 'idempotency_key'], 'properties' => $idempotencyProperty + ['occurrence_id' => ['type' => 'integer']]]], + ['name' => 'market_management.reactivate_occurrence', 'description' => 'Marktdurchführung reaktivieren', 'inputSchema' => ['type' => 'object', 'required' => ['occurrence_id', 'idempotency_key'], 'properties' => $idempotencyProperty + ['occurrence_id' => ['type' => 'integer']]]], + ['name' => 'market_management.create_day', 'description' => 'Markttag anlegen', 'inputSchema' => ['type' => 'object', 'required' => ['occurrence_id', 'date', 'idempotency_key'], 'properties' => $idempotencyProperty + ['occurrence_id' => ['type' => 'integer']] + $dayProperties]], + ['name' => 'market_management.update_day', 'description' => 'Markttag ändern', 'inputSchema' => ['type' => 'object', 'required' => ['day_id', 'idempotency_key'], 'properties' => $idempotencyProperty + ['day_id' => ['type' => 'integer']] + $dayProperties]], + ['name' => 'market_management.delete_day', 'description' => 'Markttag entfernen', 'inputSchema' => ['type' => 'object', 'required' => ['day_id', 'idempotency_key'], 'properties' => $idempotencyProperty + ['day_id' => ['type' => 'integer']]]], ]; } diff --git a/scripts/tests/market_management_mcp_tools_list_test.php b/scripts/tests/market_management_mcp_tools_list_test.php index 8b74e5a..1f1ccb7 100644 --- a/scripts/tests/market_management_mcp_tools_list_test.php +++ b/scripts/tests/market_management_mcp_tools_list_test.php @@ -79,6 +79,21 @@ assert_test(is_array($createOccurrenceTool), 'Create occurrence MCP tool must ex $createProperties = $createOccurrenceTool['inputSchema']['properties']; assert_test(isset($createProperties['place'], $createProperties['days']['items']['properties']['date']), 'Create occurrence schema must expose both levels'); assert_test(!isset($createProperties['days']['items']['properties']['place']), 'Occurrence fields must not be nested under days'); +$writeTools = [ + 'market_management.create_market', 'market_management.update_market', + 'market_management.create_market_organizer', 'market_management.create_municipality', + 'market_management.archive_market', 'market_management.reactivate_market', + 'market_management.create_occurrence', 'market_management.update_occurrence', + 'market_management.update_occurrence_checklist', 'market_management.archive_occurrence', + 'market_management.reactivate_occurrence', 'market_management.create_day', + 'market_management.update_day', 'market_management.delete_day', +]; +foreach ($writeTools as $writeToolName) { + $writeTool = array_values(array_filter($tools, static fn (array $tool): bool => $tool['name'] === $writeToolName))[0] ?? null; + assert_test(is_array($writeTool), $writeToolName . ' must exist'); + assert_test(in_array('idempotency_key', $writeTool['inputSchema']['required'] ?? [], true), $writeToolName . ' must require idempotency_key'); + assert_test(isset($writeTool['inputSchema']['properties']['idempotency_key']), $writeToolName . ' must expose idempotency_key'); +} $actual = []; foreach ($tools as $tool) {