['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']], 'gross_cost' => ['type' => ['number', 'null'], 'minimum' => 0], 'tax_status' => ['type' => ['string', 'null'], 'enum' => ['tax_free', 'taxable']], 'tax_rate' => ['type' => ['number', 'null'], 'minimum' => 0], 'cost_items_text' => ['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'], 'contact_person_name' => ['type' => ['string', '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', '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'], 'format' => 'uri']]]], ['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'], 'format' => 'uri']]]], ['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.list_categories', 'description' => 'Bestehende Kategorien lesen', 'inputSchema' => ['type' => 'object', 'properties' => []]], ['name' => 'market_management.list_market_organizers', 'description' => 'Aktive Veranstalter lesen', 'inputSchema' => ['type' => 'object', 'properties' => []]], ['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.list_reference_data', 'description' => 'Kategorien, Kantone und Gemeinden lesen', 'inputSchema' => ['type' => 'object', 'properties' => []]], ['name' => 'market_management.create_municipality', 'description' => 'Lokale Gemeinde im Markt-Gemeinderegister anlegen', 'inputSchema' => ['type' => 'object', 'required' => ['name'], 'properties' => ['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'], 'days' => ['type' => 'array', 'minItems' => 1, 'items' => ['type' => 'object', 'required' => ['date'], 'properties' => $dayProperties]] + $occurrenceProperties]]], ['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.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']]]], ]; } function market_management_mcp_is_write_tool(string $tool): bool { return in_array($tool, [ '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.archive_occurrence', 'market_management.reactivate_occurrence', 'market_management.create_day', 'market_management.update_day', 'market_management.delete_day', ], true); } function market_management_mcp_call_idempotent(PDO $pdo, string $userSubject, string $tool, array $arguments): array { if (!market_management_mcp_is_write_tool($tool)) { return market_management_mcp_call($pdo, $tool, $arguments); } $idempotencyKey = trim((string) ($arguments['idempotency_key'] ?? '')); if ($idempotencyKey === '') { throw new InvalidArgumentException('Schreibende MCP-Aufrufe benötigen idempotency_key.'); } $clientId = trim((string) ($arguments['client_id'] ?? 'hermes')); if ($clientId === '') { throw new InvalidArgumentException('client_id darf nicht leer sein.'); } $businessArguments = $arguments; unset($businessArguments['idempotency_key'], $businessArguments['client_id']); $requestHash = hash('sha256', json_encode([ 'tool' => $tool, 'arguments' => $businessArguments, ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR)); $pdo->beginTransaction(); try { $existingStmt = $pdo->prepare( 'SELECT user_subject, request_hash, result_status, result_payload FROM market_mcp_idempotency WHERE client_id = :client_id AND idempotency_key = :idempotency_key FOR UPDATE' ); $existingStmt->execute([':client_id' => $clientId, ':idempotency_key' => $idempotencyKey]); $existing = $existingStmt->fetch(); if (is_array($existing)) { if ((string) $existing['user_subject'] !== $userSubject || (string) $existing['request_hash'] !== $requestHash) { throw new InvalidArgumentException('Idempotency-Schlüssel wurde bereits für eine andere Anfrage verwendet.'); } if ((string) $existing['result_status'] === 'succeeded' && $existing['result_payload'] !== null) { $pdo->commit(); return (array) json_decode((string) $existing['result_payload'], true, 512, JSON_THROW_ON_ERROR); } throw new RuntimeException('Idempotency-Anfrage wird bereits verarbeitet oder ist fehlgeschlagen.'); } $insert = $pdo->prepare( 'INSERT INTO market_mcp_idempotency (user_subject, client_id, idempotency_key, tool_name, request_hash, result_status, expires_at) VALUES (:user_subject, :client_id, :idempotency_key, :tool_name, :request_hash, \'processing\', NOW() + INTERVAL \'24 hours\')' ); $insert->execute([ ':user_subject' => $userSubject, ':client_id' => $clientId, ':idempotency_key' => $idempotencyKey, ':tool_name' => $tool, ':request_hash' => $requestHash, ]); $result = market_management_mcp_call($pdo, $tool, $businessArguments); $update = $pdo->prepare( 'UPDATE market_mcp_idempotency SET result_status = \'succeeded\', result_payload = CAST(:result_payload AS JSONB) WHERE client_id = :client_id AND idempotency_key = :idempotency_key' ); $update->execute([ ':result_payload' => json_encode($result, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR), ':client_id' => $clientId, ':idempotency_key' => $idempotencyKey, ]); $pdo->commit(); return $result; } catch (Throwable $e) { if ($pdo->inTransaction()) { $pdo->rollBack(); } throw $e; } } 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('Marktdurchfü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 { $simpleCostFields = []; foreach (['gross_cost', 'cost_items_text', 'tax_status', 'tax_rate'] as $field) { if (array_key_exists($field, $input)) { $simpleCostFields[$field] = $input[$field]; unset($input[$field]); } } if ($simpleCostFields !== []) { market_management_update_occurrence_simple_costs($pdo, $occurrenceId, $simpleCostFields); } $contactFields = []; foreach (['contact_person_name', 'contact_email', 'contact_phone'] as $field) { if (array_key_exists($field, $input)) { $contactFields[$field] = $input[$field]; unset($input[$field]); } } $existing = null; if ($contactFields !== []) { $existing = market_management_get_occurrence($pdo, $occurrenceId, true); if ($existing === null) { throw new RuntimeException('Marktdurchführung nicht gefunden.'); } } if ($contactFields !== []) { $contactFields['occurrence_id'] = $occurrenceId; market_management_set_occurrence_contact($pdo, $contactFields); } 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_mcp_get_market( $pdo, market_management_require_positive_id($arguments['market_id'] ?? null, 'market_id'), (bool) ($arguments['include_archived'] ?? false) ), 'market_management.create_market' => market_management_create_market($pdo, $arguments), 'market_management.update_market' => market_management_update_market( $pdo, market_management_require_positive_id($arguments['market_id'] ?? null, 'market_id'), $arguments ), 'market_management.archive_market' => market_management_set_market_archive( $pdo, market_management_require_positive_id($arguments['market_id'] ?? null, 'market_id'), true ), 'market_management.reactivate_market' => market_management_set_market_archive( $pdo, market_management_require_positive_id($arguments['market_id'] ?? null, 'market_id'), false ), 'market_management.list_categories' => market_management_list_categories($pdo), 'market_management.list_market_organizers' => market_management_list_organizers($pdo), 'market_management.create_market_organizer' => market_management_create_organizer( $pdo, (string) ($arguments['name'] ?? ''), $arguments['email'] ?? null, $arguments['phone'] ?? null ), 'market_management.list_reference_data' => market_management_list_reference_data($pdo, $arguments), 'market_management.create_municipality' => market_management_create_municipality( $pdo, (string) ($arguments['name'] ?? '') ), 'market_management.get_overview' => [ 'markets' => market_management_search_markets($pdo, $arguments), 'occurrences' => market_management_search_occurrences($pdo, $arguments), ], 'market_management.create_occurrence' => market_management_create_occurrence($pdo, $arguments), '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) ), 'market_management.update_occurrence' => market_management_mcp_update_occurrence( $pdo, market_management_require_positive_id($arguments['occurrence_id'] ?? null, 'occurrence_id'), $arguments ), 'market_management.archive_occurrence' => market_management_set_occurrence_archive( $pdo, market_management_require_positive_id($arguments['occurrence_id'] ?? null, 'occurrence_id'), true ), 'market_management.reactivate_occurrence' => market_management_set_occurrence_archive( $pdo, market_management_require_positive_id($arguments['occurrence_id'] ?? null, 'occurrence_id'), false ), 'market_management.create_day' => market_management_create_day($pdo, $arguments), 'market_management.update_day' => market_management_update_day( $pdo, market_management_require_positive_id($arguments['day_id'] ?? null, 'day_id'), $arguments ), 'market_management.delete_day' => market_management_delete_day( $pdo, market_management_require_positive_id($arguments['day_id'] ?? null, 'day_id') ), default => throw new InvalidArgumentException('Unbekanntes MCP-Tool: ' . $tool), }; }