['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', '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'], '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_reference_data', 'description' => 'Kategorien, Kantone und Gemeinden lesen', 'inputSchema' => ['type' => 'object', 'properties' => []]], ['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'], 'properties' => ['occurrence_id' => ['type' => 'integer']]]], ['name' => 'market_management.reactivate_occurrence', 'description' => 'Durchführung reaktivieren', 'inputSchema' => ['type' => 'object', 'required' => ['occurrence_id'], 'properties' => ['occurrence_id' => ['type' => 'integer']]]], ['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'], 'properties' => ['day_id' => ['type' => 'integer']]]], ['name' => 'market_management.set_organizer', 'description' => 'Veranstalter einer Durchführung setzen', 'inputSchema' => ['type' => 'object', 'required' => ['occurrence_id', 'organization_contact_id', 'contact_person_id'], 'properties' => ['occurrence_id' => ['type' => 'integer'], 'organization_contact_id' => ['type' => 'integer'], 'contact_person_id' => ['type' => 'integer']]]], ['name' => 'market_management.delete_organizer', 'description' => 'Veranstalter einer Durchführung entfernen', 'inputSchema' => ['type' => 'object', 'required' => ['occurrence_id'], 'properties' => ['occurrence_id' => ['type' => 'integer']]]], ['name' => 'market_management.create_cost_item', 'description' => 'Kostenposition anlegen', 'inputSchema' => ['type' => 'object', 'required' => ['occurrence_id', 'cost_type', 'gross_amount', 'tax_status'], 'properties' => ['occurrence_id' => ['type' => 'integer'], 'cost_type' => ['type' => 'string'], 'description' => ['type' => ['string', 'null']], 'gross_amount' => ['type' => 'number'], 'tax_status' => ['type' => 'string', 'enum' => ['tax_free', 'taxable']], 'tax_rate' => ['type' => 'number']]]], ['name' => 'market_management.update_cost_item', 'description' => 'Kostenposition ändern', 'inputSchema' => ['type' => 'object', 'required' => ['cost_item_id'], 'properties' => ['cost_item_id' => ['type' => 'integer'], 'cost_type' => ['type' => 'string'], 'description' => ['type' => ['string', 'null']], 'gross_amount' => ['type' => ['number', 'null']], 'tax_status' => ['type' => ['string', 'null'], 'enum' => ['tax_free', 'taxable']], 'tax_rate' => ['type' => ['number', 'null']]]]], ['name' => 'market_management.delete_cost_item', 'description' => 'Kostenposition entfernen', 'inputSchema' => ['type' => 'object', 'required' => ['cost_item_id'], 'properties' => ['cost_item_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.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', 'market_management.set_organizer', 'market_management.delete_organizer', 'market_management.create_cost_item', 'market_management.update_cost_item', 'market_management.delete_cost_item', ], 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('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_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_reference_data' => market_management_list_reference_data($pdo, $arguments), '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') ), 'market_management.set_organizer' => market_management_set_organizer($pdo, $arguments), 'market_management.delete_organizer' => market_management_delete_organizer( $pdo, market_management_require_positive_id($arguments['occurrence_id'] ?? null, 'occurrence_id') ), 'market_management.create_cost_item' => market_management_create_cost_item($pdo, $arguments), 'market_management.update_cost_item' => market_management_update_cost_item( $pdo, market_management_require_positive_id($arguments['cost_item_id'] ?? null, 'cost_item_id'), $arguments ), 'market_management.delete_cost_item' => market_management_delete_cost_item( $pdo, market_management_require_positive_id($arguments['cost_item_id'] ?? null, 'cost_item_id') ), default => throw new InvalidArgumentException('Unbekanntes MCP-Tool: ' . $tool), }; }