string('category')->toString(); $search = $request->string('q')->toString(); $servers = McpServer::approved() ->when($category, fn ($query) => $query->where('category', $category)) ->when($search, fn ($query) => $query->where('name', 'like', "%{$search}%")) ->orderByDesc('featured') ->orderByDesc('views') ->paginate(12) ->withQueryString(); return view('mcp-servers.index', [ 'servers' => $servers, 'categories' => config('mcp_servers.categories'), 'activeCategory' => $category, 'search' => $search, 'title' => 'MCP Servers Directory — '.config('app.name'), 'description' => 'Discover Model Context Protocol servers, with docs, install instructions, and config examples.', 'canonical' => route('mcp-servers.index'), ]); } public function category(string $category): View { abort_unless(array_key_exists($category, config('mcp_servers.categories')), 404); $servers = McpServer::approved() ->where('category', $category) ->orderByDesc('featured') ->orderByDesc('views') ->paginate(12); $label = config("mcp_servers.categories.{$category}"); return view('mcp-servers.category', [ 'servers' => $servers, 'categories' => config('mcp_servers.categories'), 'category' => $category, 'label' => $label, 'title' => $label.' MCP Servers — '.config('app.name'), 'description' => "MCP servers for {$label}.", 'canonical' => route('mcp-servers.category', $category), ]); } public function show(McpServer $mcpServer): View { abort_unless($mcpServer->approved, 404); $mcpServer->increment('views'); $related = McpServer::approved() ->where('category', $mcpServer->category) ->whereKeyNot($mcpServer->id) ->limit(4) ->get(); return view('mcp-servers.show', [ 'server' => $mcpServer, 'related' => $related, 'title' => $mcpServer->name.' — MCP Servers — '.config('app.name'), 'description' => $this->metaDescription($mcpServer->description), 'canonical' => route('mcp-servers.show', $mcpServer), 'image' => $mcpServer->logo_url, ]); } public function visit(McpServer $mcpServer): RedirectResponse { abort_unless($mcpServer->approved, 404); $mcpServer->increment('clicks'); return redirect()->away($mcpServer->url); } /** * Truncate to ~155 chars without cutting a word in half. Google appends * its own ellipsis when it truncates in search results, so we don't add one. */ private function metaDescription(string $description, int $limit = 155): string { if (Str::length($description) <= $limit) { return $description; } return Str::of($description)->limit($limit, '')->beforeLast(' ')->toString(); } }