'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.create_market', 'description' => 'Markt mit bestehender Kategorie anlegen', 'inputSchema' => ['type' => 'object', 'required' => ['name', 'category_id']]], ['name' => 'market_management.update_market', 'description' => 'Markt ändern', 'inputSchema' => ['type' => 'object', 'required' => ['market_id']]], ['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.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.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']]], ['name' => 'market_management.create_cost_item', 'description' => 'Kostenposition anlegen', 'inputSchema' => ['type' => 'object', 'required' => ['occurrence_id', 'cost_type', 'gross_amount', 'tax_status']]], ['name' => 'market_management.update_cost_item', 'description' => 'Kostenposition ändern', 'inputSchema' => ['type' => 'object', 'required' => ['cost_item_id']]], ['name' => 'market_management.delete_cost_item', 'description' => 'Kostenposition entfernen', 'inputSchema' => ['type' => 'object', 'required' => ['cost_item_id']]], ]; } 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_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( $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, 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_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( $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), }; }