feat(changelog): Hook lit_mcp_changed

ChangeLog::record() löst nach jeder erfolgreichen oder teilweise gespeicherten Änderung lit_mcp_changed(ability, objectType, objectId, logId) aus, nie bei abgelehnten. Die Werte sind gekürzt wie im Protokoll; Ausnahmen von Listenern werden protokolliert und ändern das Ergebnis des Tools nicht.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Jan-Niclas Loosen
2026-11-06 14:50:12 +01:00
co-authored by Claude Opus 5.5
parent 1a294b7881
commit c383b99e7d
2 changed files with 137 additions and 5 deletions
+36 -5
View File
@@ -18,6 +18,9 @@ class ChangeLog
{
public const TABLE = 'lit_mcp_log';
/** Fires after every write that changed something (result ok or partial), see announce(). */
public const CHANGED_ACTION = 'lit_mcp_changed';
public function __construct(private ?\wpdb $db = null)
{
}
@@ -42,15 +45,19 @@ class ChangeLog
string $result = 'ok'
): ?int {
$wpdb = $this->db();
// Shortened once: the row and the announcement carry the same values.
$ability = mb_substr($ability, 0, 100);
$objectType = mb_substr($objectType, 0, 50);
$objectId = mb_substr($objectId, 0, 191);
$inserted = $wpdb->insert(
$this->tableName(),
[
'created_at' => current_time('mysql', true),
'user_id' => get_current_user_id(),
'client' => $this->sanitizedClientName(),
'ability' => mb_substr($ability, 0, 100),
'object_type' => mb_substr($objectType, 0, 50),
'object_id' => mb_substr($objectId, 0, 191),
'ability' => $ability,
'object_type' => $objectType,
'object_id' => $objectId,
'summary' => mb_substr($summary, 0, 500),
'revision_id' => $revisionId,
'snapshot_id' => $snapshotId,
@@ -59,12 +66,36 @@ class ChangeLog
['%s', '%d', '%s', '%s', '%s', '%s', '%s', '%d', '%s', '%s']
);
$logId = null;
if ($inserted === false) {
Log::line(sprintf('change log insert failed for %s %s:%s', $ability, $objectType, $objectId));
return null;
} else {
$logId = (int) $wpdb->insert_id;
}
return (int) $wpdb->insert_id;
if ($result !== 'refused') {
self::announce($ability, $objectType, $objectId, $logId);
}
return $logId;
}
/**
* do_action('lit_mcp_changed', ability, objectType, objectId, logId): a write happened (result ok or partial,
* never refused), even if its log row did not (logId null).
*
* ability, objectType and objectId are shortened like the log row (100/50/191 characters). objectId is an
* identifier or name from the tool input (post ID, option name, menu location ...): untrusted text, validate
* before use. A listener that throws is caught and logged, the caller must not see a failed write; exit and
* wp_die in a listener are not caught and end the request.
*/
private static function announce(string $ability, string $objectType, string $objectId, ?int $logId): void
{
try {
do_action(self::CHANGED_ACTION, $ability, $objectType, $objectId, $logId);
} catch (\Throwable $e) {
Log::failure(self::CHANGED_ACTION, $e);
}
}
/**