Laravel MCP Client Implementation
Complete guide to implementing MCP clients in Laravel with multiple server connections, caching, and production-ready patterns.
Multiple Client Configuration
Configure multiple named clients in config/mcp.php
:
php
'default_client' => 'main',
'clients' => [
'main' => [
'name' => 'Laravel Main Client',
'version' => '1.0.0',
'capabilities' => ['roots', 'sampling'],
'timeout' => 30,
'retry_attempts' => 3,
],
'external_api' => [
'name' => 'External API Client',
'version' => '1.0.0',
'capabilities' => ['tools', 'resources'],
'timeout' => 60,
'retry_attempts' => 5,
'auth' => [
'type' => 'bearer',
'token' => env('EXTERNAL_API_TOKEN'),
],
],
'openai_integration' => [
'name' => 'OpenAI Integration Client',
'version' => '1.0.0',
'capabilities' => ['tools', 'resources', 'prompts'],
'timeout' => 120,
'auth' => [
'type' => 'oauth',
'client_id' => env('OPENAI_CLIENT_ID'),
'client_secret' => env('OPENAI_CLIENT_SECRET'),
],
],
],
Using Multiple Clients
Basic Client Operations
php
use MCP\Laravel\Facades\McpClient;
// Connect to multiple servers
McpClient::connect('main', 'stdio://./local-server.php');
McpClient::connect('external_api', 'http://api.example.com:3000');
McpClient::connect('openai_integration', 'http://localhost:3001');
// Call tools on different servers
$localResult = McpClient::callTool('main', 'calculate', ['a' => 5, 'b' => 3]);
$apiResult = McpClient::callTool('external_api', 'weather', ['city' => 'London']);
$aiResult = McpClient::callTool('openai_integration', 'analyze_text', ['text' => 'Hello world']);
// List tools from specific client
$tools = McpClient::listTools('external_api');
// Read resources
$configResource = McpClient::readResource('main', 'config://app.name');
$apiResource = McpClient::readResource('external_api', 'data://users/123');
// Get prompts
$prompt = McpClient::getPrompt('openai_integration', 'code_review', [
'code' => $code,
'language' => 'php',
]);
Advanced Client Service
php
<?php
namespace App\Services;
use MCP\Laravel\Facades\McpClient;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log;
class AdvancedMcpClientService
{
private array $connectionPool = [];
private array $healthStatus = [];
/**
* Initialize client connections with health monitoring
*/
public function initializeConnections(): void
{
$clients = config('mcp.clients', []);
foreach ($clients as $name => $config) {
try {
$this->connectWithRetry($name, $config);
$this->healthStatus[$name] = 'healthy';
Log::info("MCP Client connected", ['client' => $name]);
} catch (\Exception $e) {
$this->healthStatus[$name] = 'failed';
Log::error("MCP Client connection failed", [
'client' => $name,
'error' => $e->getMessage(),
]);
}
}
}
/**
* Connect with retry logic
*/
private function connectWithRetry(string $clientName, array $config): void
{
$maxRetries = $config['retry_attempts'] ?? 3;
$retryDelay = 1; // seconds
for ($attempt = 1; $attempt <= $maxRetries; $attempt++) {
try {
$serverUrl = $this->resolveServerUrl($clientName, $config);
McpClient::connect($clientName, $serverUrl, null, $config['auth'] ?? []);
return;
} catch (\Exception $e) {
if ($attempt === $maxRetries) {
throw $e;
}
Log::warning("MCP Client connection attempt {$attempt} failed", [
'client' => $clientName,
'error' => $e->getMessage(),
]);
sleep($retryDelay);
$retryDelay *= 2; // Exponential backoff
}
}
}
/**
* Execute tool with fallback and caching
*/
public function executeToolWithFallback(string $toolName, array $params, array $clientPriority = null): array
{
$clients = $clientPriority ?: $this->getHealthyClients();
$cacheKey = $this->buildToolCacheKey($toolName, $params);
// Check cache first
if ($cached = Cache::get($cacheKey)) {
return array_merge($cached, ['_cache_hit' => true]);
}
$lastException = null;
foreach ($clients as $clientName) {
try {
if (!$this->isClientHealthy($clientName)) {
continue;
}
$result = McpClient::callTool($clientName, $toolName, $params);
// Cache successful results
$ttl = $this->getToolCacheTtl($toolName);
if ($ttl > 0) {
Cache::put($cacheKey, $result, $ttl);
}
Log::info("Tool executed successfully", [
'tool' => $toolName,
'client' => $clientName,
]);
return array_merge($result, [
'_client_used' => $clientName,
'_cache_hit' => false,
]);
} catch (\Exception $e) {
$lastException = $e;
$this->markClientUnhealthy($clientName);
Log::warning("Tool execution failed, trying next client", [
'tool' => $toolName,
'client' => $clientName,
'error' => $e->getMessage(),
]);
}
}
throw new \Exception('All clients failed to execute tool: ' . $lastException?->getMessage());
}
/**
* Batch execute multiple operations
*/
public function batchExecute(array $operations): array
{
$results = [];
$futures = [];
foreach ($operations as $id => $operation) {
$futures[$id] = $this->executeAsync($operation);
}
// Wait for all operations to complete
foreach ($futures as $id => $future) {
try {
$results[$id] = $future->await();
} catch (\Exception $e) {
$results[$id] = [
'error' => $e->getMessage(),
'operation' => $operations[$id],
];
}
}
return $results;
}
/**
* Execute operation asynchronously
*/
private function executeAsync(array $operation)
{
return \Amp\async(function () use ($operation) {
switch ($operation['type']) {
case 'tool':
return $this->executeToolWithFallback(
$operation['name'],
$operation['params'] ?? [],
$operation['clients'] ?? null
);
case 'resource':
return $this->readResourceWithFallback(
$operation['uri'],
$operation['clients'] ?? null
);
case 'prompt':
return $this->getPromptWithFallback(
$operation['name'],
$operation['args'] ?? [],
$operation['clients'] ?? null
);
default:
throw new \InvalidArgumentException("Unknown operation type: {$operation['type']}");
}
});
}
/**
* Read resource with fallback
*/
public function readResourceWithFallback(string $uri, array $clientPriority = null): array
{
$clients = $clientPriority ?: $this->getHealthyClients();
$cacheKey = "resource:" . md5($uri);
// Check cache first
if ($cached = Cache::get($cacheKey)) {
return array_merge($cached, ['_cache_hit' => true]);
}
foreach ($clients as $clientName) {
try {
if (!$this->isClientHealthy($clientName)) {
continue;
}
$result = McpClient::readResource($clientName, $uri);
// Cache successful results
Cache::put($cacheKey, $result, 900); // 15 minutes
return array_merge($result, [
'_client_used' => $clientName,
'_cache_hit' => false,
]);
} catch (\Exception $e) {
Log::warning("Resource read failed, trying next client", [
'uri' => $uri,
'client' => $clientName,
'error' => $e->getMessage(),
]);
}
}
throw new \Exception("All clients failed to read resource: {$uri}");
}
/**
* Stream large resources with progress tracking
*/
public function streamResource(string $clientName, string $uri, callable $progressCallback = null): \Generator
{
if (!$this->isClientHealthy($clientName)) {
throw new \Exception("Client {$clientName} is not healthy");
}
$client = McpClient::get($clientName);
// For large resources, we might need to implement streaming
// This is a conceptual implementation
$totalSize = $this->getResourceSize($clientName, $uri);
$chunkSize = 8192;
$downloaded = 0;
while ($downloaded < $totalSize) {
$chunk = $this->readResourceChunk($clientName, $uri, $downloaded, $chunkSize);
$downloaded += strlen($chunk);
if ($progressCallback) {
$progressCallback($downloaded, $totalSize);
}
yield $chunk;
}
}
/**
* Health monitoring and recovery
*/
public function performHealthCheck(): array
{
$results = [];
$clients = array_keys(config('mcp.clients', []));
foreach ($clients as $clientName) {
try {
$startTime = microtime(true);
McpClient::ping($clientName);
$responseTime = (microtime(true) - $startTime) * 1000;
$this->healthStatus[$clientName] = 'healthy';
$results[$clientName] = [
'status' => 'healthy',
'response_time_ms' => round($responseTime, 2),
'last_check' => now(),
];
} catch (\Exception $e) {
$this->healthStatus[$clientName] = 'unhealthy';
$results[$clientName] = [
'status' => 'unhealthy',
'error' => $e->getMessage(),
'last_check' => now(),
];
}
}
// Cache health status
Cache::put('mcp_client_health', $results, 60);
return $results;
}
/**
* Auto-reconnect unhealthy clients
*/
public function attemptReconnection(): array
{
$results = [];
$unhealthyClients = array_filter($this->healthStatus, fn($status) => $status !== 'healthy');
foreach (array_keys($unhealthyClients) as $clientName) {
try {
$config = config("mcp.clients.{$clientName}");
if ($config) {
$this->connectWithRetry($clientName, $config);
$this->healthStatus[$clientName] = 'healthy';
$results[$clientName] = 'reconnected';
}
} catch (\Exception $e) {
$results[$clientName] = 'failed: ' . $e->getMessage();
}
}
return $results;
}
/**
* Get metrics for monitoring
*/
public function getMetrics(): array
{
$clients = array_keys(config('mcp.clients', []));
$metrics = [
'total_clients' => count($clients),
'healthy_clients' => 0,
'unhealthy_clients' => 0,
'clients' => [],
];
foreach ($clients as $clientName) {
$client = McpClient::get($clientName);
$status = $client->getStatus();
$clientMetrics = [
'name' => $clientName,
'connected' => $status['connected'],
'requests' => $status['request_count'],
'errors' => $status['error_count'],
'last_response_time' => $status['last_response_time'],
'uptime' => $status['uptime'],
];
$metrics['clients'][$clientName] = $clientMetrics;
if ($status['connected']) {
$metrics['healthy_clients']++;
} else {
$metrics['unhealthy_clients']++;
}
}
return $metrics;
}
// Helper methods
private function getHealthyClients(): array
{
return array_keys(array_filter($this->healthStatus, fn($status) => $status === 'healthy'));
}
private function isClientHealthy(string $clientName): bool
{
return ($this->healthStatus[$clientName] ?? 'unknown') === 'healthy';
}
private function markClientUnhealthy(string $clientName): void
{
$this->healthStatus[$clientName] = 'unhealthy';
}
private function buildToolCacheKey(string $toolName, array $params): string
{
return "tool:{$toolName}:" . md5(serialize($params));
}
private function getToolCacheTtl(string $toolName): int
{
// Different TTL for different tools
return match ($toolName) {
'weather' => 1800, // 30 minutes
'database_query' => 300, // 5 minutes
'file_analysis' => 3600, // 1 hour
default => 600, // 10 minutes
};
}
private function resolveServerUrl(string $clientName, array $config): string
{
// Resolve server URL from config or service discovery
return $config['server_url'] ?? env("MCP_{$clientName}_URL", 'http://localhost:3000');
}
}
Client Middleware and Interceptors
Request/Response Interceptor
php
<?php
namespace App\Services\Mcp;
class ClientInterceptor
{
/**
* Intercept and modify requests before sending
*/
public function beforeRequest(string $clientName, string $method, array $params): array
{
// Add authentication headers
if ($auth = $this->getAuthHeaders($clientName)) {
$params['_headers'] = array_merge($params['_headers'] ?? [], $auth);
}
// Add request tracking
$params['_request_id'] = uniqid('req_');
$params['_timestamp'] = microtime(true);
Log::info("MCP Request", [
'client' => $clientName,
'method' => $method,
'request_id' => $params['_request_id'],
]);
return $params;
}
/**
* Intercept and process responses
*/
public function afterResponse(string $clientName, string $method, array $params, array $response): array
{
$requestId = $params['_request_id'] ?? 'unknown';
$startTime = $params['_timestamp'] ?? microtime(true);
$duration = microtime(true) - $startTime;
Log::info("MCP Response", [
'client' => $clientName,
'method' => $method,
'request_id' => $requestId,
'duration_ms' => round($duration * 1000, 2),
'response_size' => strlen(json_encode($response)),
]);
// Transform response if needed
$response['_metadata'] = [
'client' => $clientName,
'request_id' => $requestId,
'duration_ms' => round($duration * 1000, 2),
];
return $response;
}
/**
* Handle request errors
*/
public function onError(string $clientName, string $method, array $params, \Exception $error): void
{
$requestId = $params['_request_id'] ?? 'unknown';
Log::error("MCP Request Error", [
'client' => $clientName,
'method' => $method,
'request_id' => $requestId,
'error' => $error->getMessage(),
'trace' => $error->getTraceAsString(),
]);
// Could trigger alerts, circuit breakers, etc.
$this->handleErrorRecovery($clientName, $error);
}
private function getAuthHeaders(string $clientName): array
{
$config = config("mcp.clients.{$clientName}.auth");
if (!$config) {
return [];
}
return match ($config['type']) {
'bearer' => ['Authorization' => 'Bearer ' . $config['token']],
'api_key' => ['X-API-Key' => $config['key']],
'oauth' => $this->getOAuthHeaders($config),
default => [],
};
}
private function getOAuthHeaders(array $config): array
{
// Implement OAuth token retrieval logic
$token = Cache::remember("oauth_token_{$config['client_id']}", 3600, function () use ($config) {
// Get OAuth token
return $this->getOAuthToken($config);
});
return ['Authorization' => 'Bearer ' . $token];
}
private function handleErrorRecovery(string $clientName, \Exception $error): void
{
// Implement circuit breaker pattern
$errorCount = Cache::increment("mcp_errors_{$clientName}");
if ($errorCount > 5) {
Cache::put("mcp_circuit_breaker_{$clientName}", true, 300); // 5 minutes
Log::warning("Circuit breaker activated for client", ['client' => $clientName]);
}
}
}
Connection Pooling and Load Balancing
Connection Pool Manager
php
<?php
namespace App\Services\Mcp;
use Illuminate\Support\Facades\Cache;
class ConnectionPoolManager
{
private array $pools = [];
private array $loadBalancers = [];
/**
* Initialize connection pools for high-traffic scenarios
*/
public function initializePools(): void
{
$poolConfigs = config('mcp.connection_pools', []);
foreach ($poolConfigs as $poolName => $config) {
$this->pools[$poolName] = new ConnectionPool($poolName, $config);
$this->loadBalancers[$poolName] = new LoadBalancer($config['strategy'] ?? 'round_robin');
}
}
/**
* Get a connection from the pool
*/
public function getConnection(string $poolName): ?string
{
if (!isset($this->pools[$poolName])) {
throw new \InvalidArgumentException("Pool {$poolName} not found");
}
$pool = $this->pools[$poolName];
$loadBalancer = $this->loadBalancers[$poolName];
// Get next client using load balancing strategy
$clientName = $loadBalancer->getNextClient($pool->getAvailableClients());
if (!$clientName) {
Log::warning("No available clients in pool", ['pool' => $poolName]);
return null;
}
// Mark client as in use
$pool->markClientInUse($clientName);
return $clientName;
}
/**
* Release connection back to pool
*/
public function releaseConnection(string $poolName, string $clientName): void
{
if (isset($this->pools[$poolName])) {
$this->pools[$poolName]->releaseClient($clientName);
}
}
}
class ConnectionPool
{
private string $name;
private array $config;
private array $clients = [];
private array $inUse = [];
public function __construct(string $name, array $config)
{
$this->name = $name;
$this->config = $config;
$this->initializeClients();
}
private function initializeClients(): void
{
$clientConfigs = $this->config['clients'] ?? [];
foreach ($clientConfigs as $clientName => $config) {
$this->clients[$clientName] = [
'config' => $config,
'healthy' => true,
'last_used' => null,
'request_count' => 0,
];
}
}
public function getAvailableClients(): array
{
return array_keys(array_filter($this->clients, function ($client, $name) {
return $client['healthy'] && !isset($this->inUse[$name]);
}, ARRAY_FILTER_USE_BOTH));
}
public function markClientInUse(string $clientName): void
{
$this->inUse[$clientName] = microtime(true);
$this->clients[$clientName]['last_used'] = microtime(true);
$this->clients[$clientName]['request_count']++;
}
public function releaseClient(string $clientName): void
{
unset($this->inUse[$clientName]);
}
}
class LoadBalancer
{
private string $strategy;
private int $roundRobinIndex = 0;
public function __construct(string $strategy)
{
$this->strategy = $strategy;
}
public function getNextClient(array $availableClients): ?string
{
if (empty($availableClients)) {
return null;
}
return match ($this->strategy) {
'round_robin' => $this->roundRobin($availableClients),
'least_connections' => $this->leastConnections($availableClients),
'random' => $this->random($availableClients),
'weighted' => $this->weighted($availableClients),
default => $this->roundRobin($availableClients),
};
}
private function roundRobin(array $clients): string
{
$client = $clients[$this->roundRobinIndex % count($clients)];
$this->roundRobinIndex++;
return $client;
}
private function leastConnections(array $clients): string
{
// Implementation would check current connection counts
return $clients[0]; // Simplified
}
private function random(array $clients): string
{
return $clients[array_rand($clients)];
}
private function weighted(array $clients): string
{
// Implementation would use weighted selection
return $clients[0]; // Simplified
}
}
Testing Client Implementation
Unit Tests
php
<?php
namespace Tests\Unit;
use Tests\TestCase;
use MCP\Laravel\Facades\McpClient;
use MCP\Laravel\Exceptions\ClientNotConnectedException;
class McpClientTest extends TestCase
{
public function test_client_can_connect_to_server(): void
{
McpClient::connect('test', 'stdio://test-server.php');
$this->assertTrue(McpClient::isConnected('test'));
}
public function test_client_throws_exception_when_not_connected(): void
{
$this->expectException(ClientNotConnectedException::class);
McpClient::callTool('nonexistent', 'test_tool', []);
}
public function test_client_can_list_server_capabilities(): void
{
McpClient::connect('test', 'stdio://test-server.php');
$capabilities = McpClient::getCapabilities('test');
$this->assertIsArray($capabilities);
}
public function test_client_handles_tool_errors_gracefully(): void
{
McpClient::connect('test', 'stdio://test-server.php');
$result = McpClient::callTool('test', 'nonexistent_tool', []);
// Should return error instead of throwing exception
$this->assertArrayHasKey('error', $result);
}
}
Integration Tests
php
<?php
namespace Tests\Feature;
use Tests\TestCase;
use App\Services\AdvancedMcpClientService;
class McpClientIntegrationTest extends TestCase
{
private AdvancedMcpClientService $clientService;
protected function setUp(): void
{
parent::setUp();
$this->clientService = app(AdvancedMcpClientService::class);
}
public function test_client_service_handles_failover(): void
{
// Setup multiple clients with one failing
$result = $this->clientService->executeToolWithFallback('echo', ['message' => 'test']);
$this->assertArrayHasKey('_client_used', $result);
$this->assertNotEmpty($result['content']);
}
public function test_batch_execution_works(): void
{
$operations = [
'op1' => ['type' => 'tool', 'name' => 'echo', 'params' => ['message' => 'test1']],
'op2' => ['type' => 'tool', 'name' => 'echo', 'params' => ['message' => 'test2']],
];
$results = $this->clientService->batchExecute($operations);
$this->assertCount(2, $results);
$this->assertArrayHasKey('op1', $results);
$this->assertArrayHasKey('op2', $results);
}
public function test_health_check_monitors_all_clients(): void
{
$health = $this->clientService->performHealthCheck();
$this->assertIsArray($health);
foreach ($health as $clientName => $status) {
$this->assertArrayHasKey('status', $status);
$this->assertContains($status['status'], ['healthy', 'unhealthy']);
}
}
}
Performance Tests
php
<?php
namespace Tests\Performance;
use Tests\TestCase;
use App\Services\AdvancedMcpClientService;
class ClientPerformanceTest extends TestCase
{
public function test_concurrent_client_requests(): void
{
$clientService = app(AdvancedMcpClientService::class);
$start = microtime(true);
$operations = [];
for ($i = 0; $i < 50; $i++) {
$operations["req_{$i}"] = [
'type' => 'tool',
'name' => 'echo',
'params' => ['message' => "test_{$i}"],
];
}
$results = $clientService->batchExecute($operations);
$duration = microtime(true) - $start;
$this->assertCount(50, $results);
$this->assertLessThan(10.0, $duration, 'Should handle 50 concurrent requests in under 10 seconds');
}
public function test_caching_improves_performance(): void
{
$clientService = app(AdvancedMcpClientService::class);
// First call (cache miss)
$start1 = microtime(true);
$result1 = $clientService->executeToolWithFallback('weather', ['location' => 'NYC']);
$time1 = microtime(true) - $start1;
// Second call (cache hit)
$start2 = microtime(true);
$result2 = $clientService->executeToolWithFallback('weather', ['location' => 'NYC']);
$time2 = microtime(true) - $start2;
$this->assertLessThan($time1, $time2, 'Cached call should be faster');
$this->assertTrue($result2['_cache_hit'] ?? false);
}
}
Monitoring and Observability
Client Metrics Dashboard
php
<?php
namespace App\Http\Controllers;
use App\Services\AdvancedMcpClientService;
use Illuminate\Http\Request;
class McpClientDashboardController extends Controller
{
private AdvancedMcpClientService $clientService;
public function __construct(AdvancedMcpClientService $clientService)
{
$this->clientService = $clientService;
}
public function dashboard()
{
$metrics = $this->clientService->getMetrics();
$health = $this->clientService->performHealthCheck();
return view('mcp.dashboard', compact('metrics', 'health'));
}
public function metricsApi(): array
{
return [
'metrics' => $this->clientService->getMetrics(),
'health' => $this->clientService->performHealthCheck(),
'timestamp' => now(),
];
}
public function reconnectUnhealthy()
{
$results = $this->clientService->attemptReconnection();
return response()->json([
'message' => 'Reconnection attempted',
'results' => $results,
]);
}
}
Blade Dashboard Template
php
<!-- resources/views/mcp/dashboard.blade.php -->
@extends('layouts.app')
@section('content')
<div class="container">
<h1>MCP Client Dashboard</h1>
<!-- Health Overview -->
<div class="row mb-4">
<div class="col-md-3">
<div class="card bg-success text-white">
<div class="card-body">
<h5>Healthy Clients</h5>
<h2>{{ $metrics['healthy_clients'] }}</h2>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card bg-danger text-white">
<div class="card-body">
<h5>Unhealthy Clients</h5>
<h2>{{ $metrics['unhealthy_clients'] }}</h2>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card bg-info text-white">
<div class="card-body">
<h5>Total Requests</h5>
<h2>{{ collect($metrics['clients'])->sum('requests') }}</h2>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card bg-warning text-white">
<div class="card-body">
<h5>Total Errors</h5>
<h2>{{ collect($metrics['clients'])->sum('errors') }}</h2>
</div>
</div>
</div>
</div>
<!-- Client Details -->
<div class="card">
<div class="card-header">
<h5>Client Details</h5>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table">
<thead>
<tr>
<th>Client</th>
<th>Status</th>
<th>Requests</th>
<th>Errors</th>
<th>Error Rate</th>
<th>Avg Response Time</th>
<th>Uptime</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
@foreach($metrics['clients'] as $name => $client)
<tr>
<td>{{ $name }}</td>
<td>
@if($client['connected'])
<span class="badge bg-success">Connected</span>
@else
<span class="badge bg-danger">Disconnected</span>
@endif
</td>
<td>{{ number_format($client['requests']) }}</td>
<td>{{ number_format($client['errors']) }}</td>
<td>
@php
$errorRate = $client['requests'] > 0 ? ($client['errors'] / $client['requests']) * 100 : 0;
@endphp
{{ number_format($errorRate, 2) }}%
</td>
<td>{{ number_format($client['last_response_time'] * 1000, 2) }}ms</td>
<td>
@if($client['uptime'])
{{ gmdate('H:i:s', $client['uptime']) }}
@else
N/A
@endif
</td>
<td>
<button class="btn btn-sm btn-outline-primary" onclick="reconnectClient('{{ $name }}')">
Reconnect
</button>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
</div>
</div>
<script>
function reconnectClient(clientName) {
fetch('/mcp/clients/reconnect', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content
},
body: JSON.stringify({ client: clientName })
})
.then(response => response.json())
.then(data => {
alert(`Reconnection result: ${data.results[clientName]}`);
location.reload();
});
}
// Auto-refresh every 30 seconds
setInterval(() => {
location.reload();
}, 30000);
</script>
@endsection