1: <?php
2:
3: declare(strict_types=1);
4:
5: /**
6: * This file is part of the Nexus MCP SDK package.
7: *
8: * (c) 2026 John Paul E. Balandan, CPA <paulbalandan@gmail.com>
9: *
10: * For the full copyright and license information, please view
11: * the LICENSE file that was distributed with this source code.
12: */
13:
14: namespace Nexus\Mcp\Core;
15:
16: /**
17: * Renderer for the peer-derived strings appearing in log lines, exception messages, and JSON-RPC error payloads.
18: *
19: * Never un-escape the result, since a backslash is not itself escaped.
20: */
21: final class SafeDisplay
22: {
23: private const int MAX_LENGTH = 80;
24: private const int MAX_CAUSE_LENGTH = 256;
25:
26: /**
27: * Escapes every byte outside printable ASCII as `\xNN` and caps a short identifier at `MAX_LENGTH`.
28: */
29: public static function sanitise(string $value): string
30: {
31: return self::render($value, self::MAX_LENGTH);
32: }
33:
34: /**
35: * The same at `MAX_CAUSE_LENGTH`, for a composed message or a URI.
36: */
37: public static function sanitiseCause(string $message): string
38: {
39: return self::render($message, self::MAX_CAUSE_LENGTH);
40: }
41:
42: /**
43: * The same for a string request id, passing an int one through unchanged.
44: */
45: public static function sanitiseId(int|string $id): int|string
46: {
47: return \is_string($id) ? self::render($id, self::MAX_LENGTH) : $id;
48: }
49:
50: private static function render(string $value, int $maxLength): string
51: {
52: $escaped = preg_replace_callback(
53: '/[^\x20-\x7E]/',
54: static fn(array $matches): string => \sprintf('\\x%02x', \ord($matches[0])),
55: $value,
56: ) ?? '';
57:
58: if ($maxLength < \strlen($escaped)) {
59: return substr($escaped, 0, $maxLength - 3).'...';
60: }
61:
62: return $escaped;
63: }
64: }
65: