feat: structured errors with hint, ToolResult, cursor pagination

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7
This commit is contained in:
2026-09-25 21:37:42 +02:00
co-authored by Claude Opus 5.5
parent 5434cb6767
commit f63d35c2f8
12 changed files with 631 additions and 14 deletions
+83
View File
@@ -0,0 +1,83 @@
<?php
declare(strict_types=1);
namespace LIT\MCP\Abilities;
use WP_Error;
/**
* Opaque offset cursor for list tools (spec §8.1, §11.3).
*/
final class Cursor
{
public const DEFAULT_LIMIT = 20;
public const MAX_LIMIT = 100;
public static function encode(int $offset): string
{
return rtrim(strtr(base64_encode('o:' . $offset), '+/', '-_'), '=');
}
/** @return int|null Offset, 0 for a missing cursor, null for an invalid one. */
public static function decode(?string $cursor): ?int
{
if ($cursor === null || $cursor === '') {
return 0;
}
$raw = base64_decode(strtr($cursor, '-_', '+/'), true);
if ($raw === false || preg_match('/^o:(\d{1,9})$/', $raw, $matches) !== 1) {
return null;
}
return (int) $matches[1];
}
public static function limit(mixed $limit, int $default = self::DEFAULT_LIMIT): int
{
if ($limit === null) {
return $default;
}
return max(1, min(self::MAX_LIMIT, (int) $limit));
}
/**
* @param list<mixed> $rows Rows fetched with $limit + 1.
* @return array{items: list<mixed>, next_cursor: ?string}
*/
public static function page(array $rows, int $limit, int $offset): array
{
return [
'items' => array_slice($rows, 0, $limit),
'next_cursor' => count($rows) > $limit ? self::encode($offset + $limit) : null,
];
}
public static function invalid(): WP_Error
{
return Errors::invalidInput(
'Ungültiger cursor.',
['cursor' => 'unbekanntes Format'],
'cursor unverändert aus next_cursor der letzten Antwort übernehmen oder weglassen.'
);
}
/** @return array<string, array<string, mixed>> */
public static function inputProperties(): array
{
return [
'limit' => [
'type' => 'integer',
'minimum' => 1,
'maximum' => self::MAX_LIMIT,
'description' => 'Einträge pro Seite (Standard 20).',
],
'cursor' => [
'type' => 'string',
'description' => 'next_cursor der vorherigen Antwort, um weiterzublättern.',
],
];
}
}