ensureAbility($request, 'read', $this->name)) { return $error; } $teamId = $this->resolveTeamId($request); if (is_null($teamId)) { return $this->mcpError($request, 'Invalid token.'); } $uuid = $request->get('server_uuid'); if (! is_string($uuid) || $uuid === '') { return $this->mcpError($request, 'server_uuid argument is required.'); } $minutes = filter_var($request->get('minutes', self::DEFAULT_MINUTES), FILTER_VALIDATE_INT); if ($minutes === false || $minutes < 1) { return $this->mcpError($request, 'minutes must be a positive integer.'); } $minutes = min($minutes, self::MAX_MINUTES); $server = Server::whereTeamId($teamId)->where('uuid', $uuid)->with('settings')->first(); if (! $server) { return $this->mcpError($request, "Server [{$uuid}] not found.", ['resource_uuid' => $uuid]); } if (! $server->isMetricsEnabled()) { return $this->mcpError($request, 'Metrics are not enabled for this server. Enable Sentinel metrics in the server settings first.', ['resource_uuid' => $uuid]); } /** @var SentinelMetricsClient $client */ $client = app(SentinelMetricsClient::class, ['server' => $server]); $range = match (true) { $minutes <= 1440 => '24h', $minutes <= 10080 => '7d', default => '30d', }; $sinceMs = now()->subMinutes($minutes)->getTimestampMs(); $summary = $this->safely(fn () => $client->summary()); $network = $this->safely(fn () => $client->networkHistory($range)) ?? ['rx' => [], 'tx' => []]; $series = [ 'cpu_percent' => $this->recent($this->safely(fn () => $client->history('cpu', $range)), $sinceMs), 'memory_used_bytes' => $this->recent($this->safely(fn () => $client->history('memory', $range)), $sinceMs), 'disk_used_percent' => $this->recent($this->safely(fn () => $client->history('disk', $range)), $sinceMs), 'load1' => $this->recent($this->safely(fn () => $client->history('load', $range)), $sinceMs), 'network_rx_bytes_per_sec' => $this->recent($network['rx'] ?? [], $sinceMs), 'network_tx_bytes_per_sec' => $this->recent($network['tx'] ?? [], $sinceMs), ]; $data = [ 'server_uuid' => $server->uuid, 'server_name' => $server->name, 'online' => $summary !== null, 'minutes' => $minutes, 'bucket_seconds' => intdiv(SentinelMetricsClient::bucketMs($range), 1000), 'current' => $summary === null ? null : [ 'cpu_percent' => $summary['cpu'] ?? null, 'memory_used_bytes' => $summary['memUsed'] ?? null, 'memory_total_bytes' => $summary['memTotal'] ?? null, 'disk_used_bytes' => $summary['diskUsed'] ?? null, 'disk_total_bytes' => $summary['diskTotal'] ?? null, 'disk_used_percent' => $summary['diskPercent'] ?? null, 'load1' => $summary['load1'] ?? null, 'network_rx_bytes_per_sec' => $summary['netRx'] ?? null, 'network_tx_bytes_per_sec' => $summary['netTx'] ?? null, ], 'series' => $series, 'series_format' => '[timestamp_ms, value] pairs, averaged per bucket', ]; return $this->mcpSuccess($request, $this->respond($data), ['resource_uuid' => $uuid, 'minutes' => $minutes]); } public function schema(JsonSchema $schema): array { return [ 'server_uuid' => $schema->string()->description('Server UUID.')->required(), 'minutes' => $schema->integer()->description('How many minutes of history to return (default 60, max 43200 = 30 days).'), ]; } /** * @template T * * @param callable(): T $callback * @return T|null */ private function safely(callable $callback): mixed { try { return $callback(); } catch (\Throwable) { return null; } } /** * @param array|null $points * @return array */ private function recent(?array $points, int $sinceMs): array { return array_values(array_filter($points ?? [], fn ($point): bool => is_array($point) && (int) ($point[0] ?? 0) >= $sinceMs)); } }