'object', 'properties' => [ 'settings' => [ 'type' => 'object', 'description' => 'Schlüssel → neuer Wert, mindestens einer, z. B. {"blogname": "Praxis Muster"}.', ], ], 'required' => ['settings'], 'additionalProperties' => false, ]; } public function outputSchema(): ?array { return ToolResult::schema(['previous' => ['type' => 'object']], ['previous']); } public function annotations(): array { return ['readonly' => false, 'destructive' => false, 'idempotent' => true]; } public function isDirect(): bool { return false; } public function permission(array $input): bool|WP_Error { return current_user_can('manage_options') ? true : Errors::forbidden('Keine Berechtigung für WordPress-Einstellungen (manage_options).'); } public function execute(array $input): mixed { $settings = is_array($input['settings'] ?? null) ? $input['settings'] : []; if ($settings === []) { return Errors::invalidInput('Keine Einstellungen angegeben.', ['settings' => 'mindestens ein Schlüssel']); } $errors = $this->validateAll($settings); if ($errors !== []) { return Errors::invalidInput( 'Einstellungen ungültig; nichts wurde gespeichert.', $errors, 'Werte gemäß fields korrigieren; aktuelle Werte über lit/wp-settings-get.' ); } return $this->write($settings); } /** * @param array $settings * @return array */ private function validateAll(array $settings): array { $errors = []; foreach ($settings as $key => $value) { $error = $this->whitelist->validate((string) $key, $value, $settings); if ($error !== null) { $errors[(string) $key] = $error; } } return $errors; } /** * All values are already known-valid; writes only the ones that actually change. * If a write fails partway through, the already-written keys are restored to their * previous values so the call stays all-or-nothing. * * @param array $settings * @return array * @throws WriteFailed When a write fails (after restoring what it could). */ private function write(array $settings): array { $previous = []; foreach ($settings as $key => $value) { $previous[(string) $key] = $this->whitelist->current((string) $key); } $changed = []; try { foreach ($settings as $key => $value) { $key = (string) $key; if ($this->isStored($key, $this->whitelist->normalize($key, $value))) { continue; } $this->writeOne($key, $value); $changed[] = $key; } } catch (Throwable $e) { $this->fail($changed, $previous, $e); } if ($changed === []) { return ToolResult::ok('Keine Änderung – alle Werte waren bereits gesetzt.') ->with('previous', JsonMap::of($previous)) ->toArray(); } // The settings are saved – a failure of the change log from here on is a warning only. $summary = 'Einstellungen geändert: ' . implode(', ', $changed); $warnings = []; $logId = FollowUp::run( fn () => $this->log->record($this->name(), 'option', implode(',', $changed), $summary), $this->name() . ' change log', 'Änderungsprotokoll konnte nicht geschrieben werden; die Einstellungen sind gespeichert.', $warnings ); $result = ToolResult::ok($summary . '.'); foreach ($changed as $key) { $result->changed($key, 'option', 'updated', SettingsWhitelist::SETTINGS[$key][2]); } return $result ->logId(is_int($logId) ? $logId : null) ->warnings($warnings) ->with('previous', JsonMap::of($previous)) ->toArray(); } private function writeOne(string $key, mixed $value): void { $normalized = $this->whitelist->normalize($key, $value); if ($key === 'permalink_structure') { $this->setPermalinkStructure((string) $normalized); return; } // update_option() returns false both for a genuine failure and for a value // that already matches the stored option (WordPress core short-circuits the // write in that case, see wp-includes/option.php) – get_option() afterwards // tells the two apart, the same way MetaFieldSource::holds() does for meta. if (update_option($key, $normalized) === false && ! $this->isStored($key, $normalized)) { throw new RuntimeException(sprintf('update_option(%s) failed', $key)); } } /** * Whether option $key already holds $normalized in the form WordPress stores it: * update_option() runs sanitize_option() first (blogname/blogdescription are esc_html()'d), * so the stored value is compared with the sanitized one, like WordPress compares scalars. */ private function isStored(string $key, mixed $normalized): bool { $stored = get_option($key); $expected = sanitize_option($key, $normalized); if (is_scalar($stored) && is_scalar($expected)) { return (string) $stored === (string) $expected; } return $stored == $expected; // phpcs:ignore Universal.Operators.StrictComparisons.LooseEqual } /** * A write failed; restores the keys already written to their previous values and throws * WriteFailed: write_failed (fully restored) or partial_write (some keys could not be * restored – the client is told which, so it can check and correct them; logged as partial). * * @param list $changed Keys written so far, in write order. * @param array $previous Their values before this call. * @throws WriteFailed Always. */ private function fail(array $changed, array $previous, Throwable $cause): never { $stuck = []; foreach (array_reverse($changed) as $key) { try { $this->writeOne($key, $previous[$key]); } catch (Throwable $e) { $stuck[] = $key; } } $reason = 'Eine Einstellung konnte nicht gespeichert werden.'; if ($stuck === []) { throw new WriteFailed($reason, true, null, null, $cause); } $this->log->recordPartial( $this->name(), 'option', implode(',', $stuck), 'Einstellungen nur teilweise zurückgenommen: ' . implode(', ', $stuck) ); throw new WriteFailed( $reason, false, null, null, $cause, $cause->getMessage() . '; not restored: ' . implode(', ', $stuck), sprintf('Möglicherweise auf dem neuen, nicht vollständigen Stand: %s.', implode(', ', $stuck)), 'Aktuelle Werte über lit/wp-settings-get (mcp-adapter-execute-ability) prüfen und ggf. erneut auf die ' . 'gewünschten Werte setzen.', ['keys' => $stuck] ); } private function setPermalinkStructure(string $structure): void { global $wp_rewrite; $wp_rewrite->set_permalink_structure($structure); flush_rewrite_rules(); // set_permalink_structure() has no return value to check; verify the option // itself so a failure here is caught and rolled back like any other setting. if (! $this->isStored('permalink_structure', $structure)) { throw new RuntimeException('set_permalink_structure() did not update the stored value.'); } } }