diff --git a/app/Filament/Resources/McpServers/McpServerResource.php b/app/Filament/Resources/McpServers/McpServerResource.php new file mode 100644 index 0000000..469306d --- /dev/null +++ b/app/Filament/Resources/McpServers/McpServerResource.php @@ -0,0 +1,54 @@ + ListMcpServers::route('/'), + 'create' => CreateMcpServer::route('/create'), + 'edit' => EditMcpServer::route('/{record}/edit'), + ]; + } +} diff --git a/app/Filament/Resources/McpServers/Pages/CreateMcpServer.php b/app/Filament/Resources/McpServers/Pages/CreateMcpServer.php new file mode 100644 index 0000000..804a462 --- /dev/null +++ b/app/Filament/Resources/McpServers/Pages/CreateMcpServer.php @@ -0,0 +1,11 @@ +columns(3) + ->components([ + Section::make('Details') + ->columnSpan(2) + ->components([ + TextInput::make('name') + ->required() + ->live(onBlur: true) + ->afterStateUpdated(function (string $operation, $state, callable $set) { + if ($operation === 'create') { + $set('slug', Str::slug($state)); + } + }), + TextInput::make('slug') + ->required() + ->unique(McpServer::class, 'slug', ignoreRecord: true) + ->helperText('Used in the URL, e.g. /mcp-servers/your-slug'), + Textarea::make('description') + ->required() + ->rows(4) + ->columnSpanFull(), + TextInput::make('url') + ->label('Repository / homepage URL') + ->url() + ->required(), + TextInput::make('docs_url') + ->label('Documentation URL') + ->url(), + TextInput::make('logo_url') + ->label('Logo URL') + ->url(), + Textarea::make('install_command') + ->label('Install command') + ->rows(2) + ->helperText('e.g. npx -y @modelcontextprotocol/server-filesystem /path') + ->columnSpanFull(), + Textarea::make('config_example') + ->label('Example client configuration') + ->rows(6) + ->helperText('The JSON snippet a user pastes into their MCP client config') + ->columnSpanFull(), + TagsInput::make('capabilities') + ->placeholder('Add a capability and press enter') + ->helperText('e.g. Read files, Write files, List directories') + ->columnSpanFull(), + TagsInput::make('tags') + ->placeholder('Add a tag and press enter') + ->helperText('e.g. official, read-only, requires-api-key') + ->columnSpanFull(), + ]), + Section::make('Classification') + ->columnSpan(1) + ->components([ + Select::make('category') + ->options(config('mcp_servers.categories')) + ->required() + ->searchable(), + Select::make('transport') + ->options(config('mcp_servers.transports')), + TextInput::make('language') + ->helperText('e.g. TypeScript, Python'), + ]), + Section::make('Moderation') + ->columnSpan(3) + ->columns(4) + ->components([ + Toggle::make('approved') + ->helperText('Only approved servers are shown publicly.') + ->default(false), + Toggle::make('featured') + ->helperText('Highlight this server.') + ->default(false), + Select::make('user_id') + ->label('Submitted by') + ->relationship('user', 'name') + ->searchable() + ->preload(), + TextInput::make('views') + ->numeric() + ->default(0) + ->disabled() + ->dehydrated(), + TextInput::make('clicks') + ->numeric() + ->default(0) + ->disabled() + ->dehydrated(), + ]), + ]); + } +} diff --git a/app/Filament/Resources/McpServers/Tables/McpServersTable.php b/app/Filament/Resources/McpServers/Tables/McpServersTable.php new file mode 100644 index 0000000..d4165aa --- /dev/null +++ b/app/Filament/Resources/McpServers/Tables/McpServersTable.php @@ -0,0 +1,71 @@ +columns([ + ImageColumn::make('logo_url') + ->label(''), + TextColumn::make('name') + ->searchable() + ->weight('medium'), + TextColumn::make('category') + ->formatStateUsing(fn (string $state) => config("mcp_servers.categories.{$state}", $state)) + ->badge() + ->sortable(), + TextColumn::make('transport') + ->placeholder('—'), + TextColumn::make('language') + ->placeholder('—'), + TextColumn::make('views') + ->numeric() + ->sortable(), + TextColumn::make('clicks') + ->numeric() + ->sortable(), + IconColumn::make('featured') + ->boolean() + ->sortable(), + IconColumn::make('approved') + ->boolean() + ->sortable(), + TextColumn::make('user.name') + ->label('Submitted by') + ->placeholder('—') + ->searchable(), + TextColumn::make('created_at') + ->dateTime() + ->sortable() + ->toggleable(isToggledHiddenByDefault: true), + ]) + ->defaultSort('created_at', 'desc') + ->filters([ + SelectFilter::make('category') + ->options(config('mcp_servers.categories')), + TernaryFilter::make('approved'), + TernaryFilter::make('featured'), + ]) + ->recordActions([ + EditAction::make(), + ]) + ->toolbarActions([ + BulkActionGroup::make([ + DeleteBulkAction::make(), + ]), + ]); + } +} diff --git a/app/Filament/Resources/Skills/Pages/CreateSkill.php b/app/Filament/Resources/Skills/Pages/CreateSkill.php new file mode 100644 index 0000000..1a09ab1 --- /dev/null +++ b/app/Filament/Resources/Skills/Pages/CreateSkill.php @@ -0,0 +1,11 @@ +columns(3) + ->components([ + Section::make('Details') + ->columnSpan(2) + ->components([ + TextInput::make('name') + ->required() + ->live(onBlur: true) + ->afterStateUpdated(function (string $operation, $state, callable $set) { + if ($operation === 'create') { + $set('slug', Str::slug($state)); + } + }), + TextInput::make('slug') + ->required() + ->unique(Skill::class, 'slug', ignoreRecord: true) + ->helperText('Used in the URL, e.g. /skills/your-slug'), + Textarea::make('description') + ->required() + ->rows(4) + ->columnSpanFull(), + TextInput::make('url') + ->label('Repository / homepage URL') + ->url() + ->required(), + TextInput::make('docs_url') + ->label('Documentation URL') + ->url(), + TextInput::make('logo_url') + ->label('Logo URL') + ->url(), + Textarea::make('install_instructions') + ->label('Install instructions') + ->rows(2) + ->helperText('e.g. Copy into ~/.claude/skills/ and restart Claude') + ->columnSpanFull(), + Textarea::make('example_prompt') + ->label('Example prompt') + ->rows(2) + ->helperText('A sample prompt that would trigger or use this skill') + ->columnSpanFull(), + TagsInput::make('tags') + ->placeholder('Add a tag and press enter') + ->helperText('e.g. official, anthropic, community') + ->columnSpanFull(), + ]), + Section::make('Classification') + ->columnSpan(1) + ->components([ + Select::make('category') + ->options(config('skills.categories')) + ->required() + ->searchable(), + TextInput::make('requirements') + ->helperText('e.g. Claude Code, Code Execution tool'), + ]), + Section::make('Moderation') + ->columnSpan(3) + ->columns(4) + ->components([ + Toggle::make('approved') + ->helperText('Only approved skills are shown publicly.') + ->default(false), + Toggle::make('featured') + ->helperText('Highlight this skill.') + ->default(false), + Select::make('user_id') + ->label('Submitted by') + ->relationship('user', 'name') + ->searchable() + ->preload(), + TextInput::make('views') + ->numeric() + ->default(0) + ->disabled() + ->dehydrated(), + TextInput::make('clicks') + ->numeric() + ->default(0) + ->disabled() + ->dehydrated(), + ]), + ]); + } +} diff --git a/app/Filament/Resources/Skills/SkillResource.php b/app/Filament/Resources/Skills/SkillResource.php new file mode 100644 index 0000000..10d0e0a --- /dev/null +++ b/app/Filament/Resources/Skills/SkillResource.php @@ -0,0 +1,54 @@ + ListSkills::route('/'), + 'create' => CreateSkill::route('/create'), + 'edit' => EditSkill::route('/{record}/edit'), + ]; + } +} diff --git a/app/Filament/Resources/Skills/Tables/SkillsTable.php b/app/Filament/Resources/Skills/Tables/SkillsTable.php new file mode 100644 index 0000000..5fe2781 --- /dev/null +++ b/app/Filament/Resources/Skills/Tables/SkillsTable.php @@ -0,0 +1,69 @@ +columns([ + ImageColumn::make('logo_url') + ->label(''), + TextColumn::make('name') + ->searchable() + ->weight('medium'), + TextColumn::make('category') + ->formatStateUsing(fn (string $state) => config("skills.categories.{$state}", $state)) + ->badge() + ->sortable(), + TextColumn::make('requirements') + ->placeholder('—'), + TextColumn::make('views') + ->numeric() + ->sortable(), + TextColumn::make('clicks') + ->numeric() + ->sortable(), + IconColumn::make('featured') + ->boolean() + ->sortable(), + IconColumn::make('approved') + ->boolean() + ->sortable(), + TextColumn::make('user.name') + ->label('Submitted by') + ->placeholder('—') + ->searchable(), + TextColumn::make('created_at') + ->dateTime() + ->sortable() + ->toggleable(isToggledHiddenByDefault: true), + ]) + ->defaultSort('created_at', 'desc') + ->filters([ + SelectFilter::make('category') + ->options(config('skills.categories')), + TernaryFilter::make('approved'), + TernaryFilter::make('featured'), + ]) + ->recordActions([ + EditAction::make(), + ]) + ->toolbarActions([ + BulkActionGroup::make([ + DeleteBulkAction::make(), + ]), + ]); + } +} diff --git a/app/Http/Controllers/Api/McpServerController.php b/app/Http/Controllers/Api/McpServerController.php new file mode 100644 index 0000000..78fce50 --- /dev/null +++ b/app/Http/Controllers/Api/McpServerController.php @@ -0,0 +1,58 @@ +with('user') + ->when($request->string('category')->toString(), fn ($query, $category) => $query->where('category', $category)) + ->when($request->has('approved'), fn ($query) => $query->where('approved', $request->boolean('approved'))) + ->latest('created_at') + ->paginate(20); + + return McpServerResource::collection($servers); + } + + public function store(StoreMcpServerRequest $request): McpServerResource + { + $server = McpServer::query()->create([ + 'featured' => false, + 'approved' => false, + ...$request->validated(), + 'user_id' => $request->user()->id, + ]); + + return new McpServerResource($server->refresh()->load('user')); + } + + public function show(McpServer $mcpServer): McpServerResource + { + return new McpServerResource($mcpServer->load('user')); + } + + public function update(UpdateMcpServerRequest $request, McpServer $mcpServer): McpServerResource + { + $mcpServer->update($request->validated()); + + return new McpServerResource($mcpServer->load('user')); + } + + public function destroy(McpServer $mcpServer): Response + { + $mcpServer->delete(); + + return response()->noContent(); + } +} diff --git a/app/Http/Controllers/Api/SkillController.php b/app/Http/Controllers/Api/SkillController.php new file mode 100644 index 0000000..540b316 --- /dev/null +++ b/app/Http/Controllers/Api/SkillController.php @@ -0,0 +1,58 @@ +with('user') + ->when($request->string('category')->toString(), fn ($query, $category) => $query->where('category', $category)) + ->when($request->has('approved'), fn ($query) => $query->where('approved', $request->boolean('approved'))) + ->latest('created_at') + ->paginate(20); + + return SkillResource::collection($skills); + } + + public function store(StoreSkillRequest $request): SkillResource + { + $skill = Skill::query()->create([ + 'featured' => false, + 'approved' => false, + ...$request->validated(), + 'user_id' => $request->user()->id, + ]); + + return new SkillResource($skill->refresh()->load('user')); + } + + public function show(Skill $skill): SkillResource + { + return new SkillResource($skill->load('user')); + } + + public function update(UpdateSkillRequest $request, Skill $skill): SkillResource + { + $skill->update($request->validated()); + + return new SkillResource($skill->load('user')); + } + + public function destroy(Skill $skill): Response + { + $skill->delete(); + + return response()->noContent(); + } +} diff --git a/app/Http/Controllers/McpServerController.php b/app/Http/Controllers/McpServerController.php new file mode 100644 index 0000000..f9732b7 --- /dev/null +++ b/app/Http/Controllers/McpServerController.php @@ -0,0 +1,103 @@ +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(); + } +} diff --git a/app/Http/Controllers/SitemapController.php b/app/Http/Controllers/SitemapController.php index 63fea9e..f32779f 100644 --- a/app/Http/Controllers/SitemapController.php +++ b/app/Http/Controllers/SitemapController.php @@ -3,8 +3,10 @@ namespace App\Http\Controllers; use App\Models\Category; +use App\Models\McpServer; use App\Models\Page; use App\Models\Post; +use App\Models\Skill; use App\Models\Tag; use App\Models\Tool; use Illuminate\Http\Response; @@ -18,6 +20,10 @@ class SitemapController extends Controller $tags = Tag::all(['slug', 'updated_at']); $tools = Tool::approved()->get(['slug', 'updated_at']); $toolCategories = array_keys(config('tools.categories')); + $mcpServers = McpServer::approved()->get(['slug', 'updated_at']); + $mcpServerCategories = array_keys(config('mcp_servers.categories')); + $skills = Skill::approved()->get(['slug', 'updated_at']); + $skillCategories = array_keys(config('skills.categories')); $pages = Page::published()->get(['slug', 'updated_at']); $xml = view('sitemap', [ @@ -26,6 +32,10 @@ class SitemapController extends Controller 'tags' => $tags, 'tools' => $tools, 'toolCategories' => $toolCategories, + 'mcpServers' => $mcpServers, + 'mcpServerCategories' => $mcpServerCategories, + 'skills' => $skills, + 'skillCategories' => $skillCategories, 'pages' => $pages, ])->render(); diff --git a/app/Http/Controllers/SkillController.php b/app/Http/Controllers/SkillController.php new file mode 100644 index 0000000..f4570a3 --- /dev/null +++ b/app/Http/Controllers/SkillController.php @@ -0,0 +1,103 @@ +string('category')->toString(); + $search = $request->string('q')->toString(); + + $skills = Skill::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('skills.index', [ + 'skills' => $skills, + 'categories' => config('skills.categories'), + 'activeCategory' => $category, + 'search' => $search, + 'title' => 'Claude Skills Directory — '.config('app.name'), + 'description' => 'Discover Claude Skills, with docs, install instructions, and example prompts.', + 'canonical' => route('skills.index'), + ]); + } + + public function category(string $category): View + { + abort_unless(array_key_exists($category, config('skills.categories')), 404); + + $skills = Skill::approved() + ->where('category', $category) + ->orderByDesc('featured') + ->orderByDesc('views') + ->paginate(12); + + $label = config("skills.categories.{$category}"); + + return view('skills.category', [ + 'skills' => $skills, + 'categories' => config('skills.categories'), + 'category' => $category, + 'label' => $label, + 'title' => $label.' Claude Skills — '.config('app.name'), + 'description' => "Claude Skills for {$label}.", + 'canonical' => route('skills.category', $category), + ]); + } + + public function show(Skill $skill): View + { + abort_unless($skill->approved, 404); + + $skill->increment('views'); + + $related = Skill::approved() + ->where('category', $skill->category) + ->whereKeyNot($skill->id) + ->limit(4) + ->get(); + + return view('skills.show', [ + 'skill' => $skill, + 'related' => $related, + 'title' => $skill->name.' — Claude Skills — '.config('app.name'), + 'description' => $this->metaDescription($skill->description), + 'canonical' => route('skills.show', $skill), + 'image' => $skill->logo_url, + ]); + } + + public function visit(Skill $skill): RedirectResponse + { + abort_unless($skill->approved, 404); + + $skill->increment('clicks'); + + return redirect()->away($skill->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(); + } +} diff --git a/app/Http/Requests/StoreMcpServerRequest.php b/app/Http/Requests/StoreMcpServerRequest.php new file mode 100644 index 0000000..48bf28e --- /dev/null +++ b/app/Http/Requests/StoreMcpServerRequest.php @@ -0,0 +1,40 @@ + + */ + public function rules(): array + { + return [ + 'name' => ['required', 'string', 'max:255'], + 'slug' => ['nullable', 'string', 'max:255', Rule::unique('mcp_servers', 'slug')], + 'description' => ['required', 'string'], + 'url' => ['required', 'url', 'max:255'], + 'docs_url' => ['nullable', 'url', 'max:255'], + 'logo_url' => ['nullable', 'url', 'max:255'], + 'category' => ['required', Rule::in(array_keys(config('mcp_servers.categories')))], + 'install_command' => ['nullable', 'string'], + 'config_example' => ['nullable', 'string'], + 'transport' => ['nullable', Rule::in(array_keys(config('mcp_servers.transports')))], + 'language' => ['nullable', 'string', 'max:100'], + 'capabilities' => ['sometimes', 'array'], + 'capabilities.*' => ['string', 'max:255'], + 'tags' => ['sometimes', 'array'], + 'tags.*' => ['string', 'max:50'], + 'featured' => ['sometimes', 'boolean'], + 'approved' => ['sometimes', 'boolean'], + ]; + } +} diff --git a/app/Http/Requests/StoreSkillRequest.php b/app/Http/Requests/StoreSkillRequest.php new file mode 100644 index 0000000..b3ac251 --- /dev/null +++ b/app/Http/Requests/StoreSkillRequest.php @@ -0,0 +1,37 @@ + + */ + public function rules(): array + { + return [ + 'name' => ['required', 'string', 'max:255'], + 'slug' => ['nullable', 'string', 'max:255', Rule::unique('skills', 'slug')], + 'description' => ['required', 'string'], + 'url' => ['required', 'url', 'max:255'], + 'docs_url' => ['nullable', 'url', 'max:255'], + 'logo_url' => ['nullable', 'url', 'max:255'], + 'category' => ['required', Rule::in(array_keys(config('skills.categories')))], + 'install_instructions' => ['nullable', 'string'], + 'example_prompt' => ['nullable', 'string'], + 'requirements' => ['nullable', 'string', 'max:255'], + 'tags' => ['sometimes', 'array'], + 'tags.*' => ['string', 'max:50'], + 'featured' => ['sometimes', 'boolean'], + 'approved' => ['sometimes', 'boolean'], + ]; + } +} diff --git a/app/Http/Requests/UpdateMcpServerRequest.php b/app/Http/Requests/UpdateMcpServerRequest.php new file mode 100644 index 0000000..301624e --- /dev/null +++ b/app/Http/Requests/UpdateMcpServerRequest.php @@ -0,0 +1,40 @@ + + */ + public function rules(): array + { + return [ + 'name' => ['sometimes', 'required', 'string', 'max:255'], + 'slug' => ['sometimes', 'required', 'string', 'max:255', Rule::unique('mcp_servers', 'slug')->ignore($this->route('mcp_server'))], + 'description' => ['sometimes', 'required', 'string'], + 'url' => ['sometimes', 'required', 'url', 'max:255'], + 'docs_url' => ['nullable', 'url', 'max:255'], + 'logo_url' => ['nullable', 'url', 'max:255'], + 'category' => ['sometimes', 'required', Rule::in(array_keys(config('mcp_servers.categories')))], + 'install_command' => ['nullable', 'string'], + 'config_example' => ['nullable', 'string'], + 'transport' => ['nullable', Rule::in(array_keys(config('mcp_servers.transports')))], + 'language' => ['nullable', 'string', 'max:100'], + 'capabilities' => ['sometimes', 'array'], + 'capabilities.*' => ['string', 'max:255'], + 'tags' => ['sometimes', 'array'], + 'tags.*' => ['string', 'max:50'], + 'featured' => ['sometimes', 'boolean'], + 'approved' => ['sometimes', 'boolean'], + ]; + } +} diff --git a/app/Http/Requests/UpdateSkillRequest.php b/app/Http/Requests/UpdateSkillRequest.php new file mode 100644 index 0000000..c9df082 --- /dev/null +++ b/app/Http/Requests/UpdateSkillRequest.php @@ -0,0 +1,37 @@ + + */ + public function rules(): array + { + return [ + 'name' => ['sometimes', 'required', 'string', 'max:255'], + 'slug' => ['sometimes', 'required', 'string', 'max:255', Rule::unique('skills', 'slug')->ignore($this->route('skill'))], + 'description' => ['sometimes', 'required', 'string'], + 'url' => ['sometimes', 'required', 'url', 'max:255'], + 'docs_url' => ['nullable', 'url', 'max:255'], + 'logo_url' => ['nullable', 'url', 'max:255'], + 'category' => ['sometimes', 'required', Rule::in(array_keys(config('skills.categories')))], + 'install_instructions' => ['nullable', 'string'], + 'example_prompt' => ['nullable', 'string'], + 'requirements' => ['nullable', 'string', 'max:255'], + 'tags' => ['sometimes', 'array'], + 'tags.*' => ['string', 'max:50'], + 'featured' => ['sometimes', 'boolean'], + 'approved' => ['sometimes', 'boolean'], + ]; + } +} diff --git a/app/Http/Resources/McpServerResource.php b/app/Http/Resources/McpServerResource.php new file mode 100644 index 0000000..b6db6e6 --- /dev/null +++ b/app/Http/Resources/McpServerResource.php @@ -0,0 +1,41 @@ + + */ + public function toArray(Request $request): array + { + return [ + 'id' => $this->id, + 'name' => $this->name, + 'slug' => $this->slug, + 'description' => $this->description, + 'url' => $this->url, + 'docs_url' => $this->docs_url, + 'logo_url' => $this->logo_url, + 'category' => $this->category, + 'category_label' => $this->category_label, + 'install_command' => $this->install_command, + 'config_example' => $this->config_example, + 'transport' => $this->transport, + 'transport_label' => $this->transport_label, + 'language' => $this->language, + 'capabilities' => $this->capabilities, + 'tags' => $this->tags, + 'views' => $this->views, + 'clicks' => $this->clicks, + 'featured' => $this->featured, + 'approved' => $this->approved, + 'submitted_by' => new UserResource($this->whenLoaded('user')), + 'created_at' => $this->created_at, + 'updated_at' => $this->updated_at, + ]; + } +} diff --git a/app/Http/Resources/SkillResource.php b/app/Http/Resources/SkillResource.php new file mode 100644 index 0000000..3d6383f --- /dev/null +++ b/app/Http/Resources/SkillResource.php @@ -0,0 +1,38 @@ + + */ + public function toArray(Request $request): array + { + return [ + 'id' => $this->id, + 'name' => $this->name, + 'slug' => $this->slug, + 'description' => $this->description, + 'url' => $this->url, + 'docs_url' => $this->docs_url, + 'logo_url' => $this->logo_url, + 'category' => $this->category, + 'category_label' => $this->category_label, + 'install_instructions' => $this->install_instructions, + 'example_prompt' => $this->example_prompt, + 'requirements' => $this->requirements, + 'tags' => $this->tags, + 'views' => $this->views, + 'clicks' => $this->clicks, + 'featured' => $this->featured, + 'approved' => $this->approved, + 'submitted_by' => new UserResource($this->whenLoaded('user')), + 'created_at' => $this->created_at, + 'updated_at' => $this->updated_at, + ]; + } +} diff --git a/app/Models/McpServer.php b/app/Models/McpServer.php new file mode 100644 index 0000000..ec57afb --- /dev/null +++ b/app/Models/McpServer.php @@ -0,0 +1,72 @@ + */ + use HasFactory; + + protected function casts(): array + { + return [ + 'capabilities' => 'array', + 'tags' => 'array', + 'featured' => 'boolean', + 'approved' => 'boolean', + ]; + } + + protected static function booted(): void + { + static::creating(function (McpServer $server) { + if (empty($server->slug)) { + $server->slug = Str::slug($server->name); + } + }); + } + + /** + * @return BelongsTo + */ + public function user(): BelongsTo + { + return $this->belongsTo(User::class); + } + + /** + * @param Builder $query + */ + #[Scope] + protected function approved(Builder $query): void + { + $query->where('approved', true); + } + + protected function categoryLabel(): Attribute + { + return Attribute::get(fn () => config("mcp_servers.categories.{$this->category}", $this->category)); + } + + protected function transportLabel(): Attribute + { + return Attribute::get(fn () => $this->transport ? config("mcp_servers.transports.{$this->transport}", $this->transport) : null); + } +} diff --git a/app/Models/Skill.php b/app/Models/Skill.php new file mode 100644 index 0000000..70721b5 --- /dev/null +++ b/app/Models/Skill.php @@ -0,0 +1,66 @@ + */ + use HasFactory; + + protected function casts(): array + { + return [ + 'tags' => 'array', + 'featured' => 'boolean', + 'approved' => 'boolean', + ]; + } + + protected static function booted(): void + { + static::creating(function (Skill $skill) { + if (empty($skill->slug)) { + $skill->slug = Str::slug($skill->name); + } + }); + } + + /** + * @return BelongsTo + */ + public function user(): BelongsTo + { + return $this->belongsTo(User::class); + } + + /** + * @param Builder $query + */ + #[Scope] + protected function approved(Builder $query): void + { + $query->where('approved', true); + } + + protected function categoryLabel(): Attribute + { + return Attribute::get(fn () => config("skills.categories.{$this->category}", $this->category)); + } +} diff --git a/config/mcp_servers.php b/config/mcp_servers.php new file mode 100644 index 0000000..d00a62b --- /dev/null +++ b/config/mcp_servers.php @@ -0,0 +1,42 @@ + [ + 'official' => 'Official', + 'filesystem' => 'Filesystem & Storage', + 'databases' => 'Databases', + 'developer-tools' => 'Developer Tools', + 'productivity' => 'Productivity', + 'search' => 'Search & Web', + 'browser-automation' => 'Browser Automation', + 'communication' => 'Communication', + 'data-analytics' => 'Data & Analytics', + 'other' => 'Other', + ], + + /* + |-------------------------------------------------------------------------- + | Transports + |-------------------------------------------------------------------------- + | + | The MCP transport(s) a server supports. Informational only -- not + | enforced by validation since a server can support more than one. + | + */ + 'transports' => [ + 'stdio' => 'stdio', + 'sse' => 'SSE', + 'http' => 'Streamable HTTP', + ], + +]; diff --git a/config/skills.php b/config/skills.php new file mode 100644 index 0000000..cd27b48 --- /dev/null +++ b/config/skills.php @@ -0,0 +1,24 @@ + [ + 'document-processing' => 'Document Processing', + 'development' => 'Development', + 'design' => 'Design & Creative', + 'productivity' => 'Productivity', + 'data-analysis' => 'Data & Analysis', + 'communication' => 'Communication', + 'other' => 'Other', + ], + +]; diff --git a/database/factories/McpServerFactory.php b/database/factories/McpServerFactory.php new file mode 100644 index 0000000..4250abb --- /dev/null +++ b/database/factories/McpServerFactory.php @@ -0,0 +1,52 @@ + + */ +class McpServerFactory extends Factory +{ + /** + * @return array + */ + public function definition(): array + { + $name = fake()->unique()->company().' MCP'; + $categories = array_keys(config('mcp_servers.categories')); + + return [ + 'name' => $name, + 'slug' => Str::slug($name), + 'description' => fake()->paragraph(), + 'url' => fake()->url(), + 'docs_url' => fake()->url(), + 'logo_url' => 'https://picsum.photos/seed/'.Str::slug($name).'/200', + 'category' => fake()->randomElement($categories), + 'install_command' => 'npx -y '.Str::slug($name), + 'config_example' => json_encode(['mcpServers' => [Str::slug($name) => ['command' => 'npx', 'args' => ['-y', Str::slug($name)]]]], JSON_PRETTY_PRINT), + 'transport' => fake()->randomElement(['stdio', 'sse', 'http']), + 'language' => fake()->randomElement(['TypeScript', 'Python', 'Go']), + 'capabilities' => fake()->sentences(3), + 'tags' => fake()->randomElements(['open-source', 'official', 'read-only', 'requires-api-key'], rand(1, 3)), + 'views' => fake()->numberBetween(0, 5000), + 'clicks' => fake()->numberBetween(0, 500), + 'featured' => false, + 'approved' => true, + ]; + } + + public function featured(): static + { + return $this->state(fn () => ['featured' => true]); + } + + public function pending(): static + { + return $this->state(fn () => ['approved' => false]); + } +} diff --git a/database/factories/SkillFactory.php b/database/factories/SkillFactory.php new file mode 100644 index 0000000..1d43e3f --- /dev/null +++ b/database/factories/SkillFactory.php @@ -0,0 +1,50 @@ + + */ +class SkillFactory extends Factory +{ + /** + * @return array + */ + public function definition(): array + { + $name = fake()->unique()->company().' Skill'; + $categories = array_keys(config('skills.categories')); + + return [ + 'name' => $name, + 'slug' => Str::slug($name), + 'description' => fake()->paragraph(), + 'url' => fake()->url(), + 'docs_url' => fake()->url(), + 'logo_url' => 'https://picsum.photos/seed/'.Str::slug($name).'/200', + 'category' => fake()->randomElement($categories), + 'install_instructions' => 'Copy this skill folder into ~/.claude/skills/ (or add it via a plugin marketplace) and restart Claude.', + 'example_prompt' => fake()->sentence(), + 'requirements' => fake()->randomElement(['Claude Code', 'Claude with Code Execution tool', null]), + 'tags' => fake()->randomElements(['official', 'community', 'anthropic', 'automation'], rand(1, 3)), + 'views' => fake()->numberBetween(0, 5000), + 'clicks' => fake()->numberBetween(0, 500), + 'featured' => false, + 'approved' => true, + ]; + } + + public function featured(): static + { + return $this->state(fn () => ['featured' => true]); + } + + public function pending(): static + { + return $this->state(fn () => ['approved' => false]); + } +} diff --git a/database/migrations/2026_08_10_213001_create_mcp_servers_table.php b/database/migrations/2026_08_10_213001_create_mcp_servers_table.php new file mode 100644 index 0000000..f056d0b --- /dev/null +++ b/database/migrations/2026_08_10_213001_create_mcp_servers_table.php @@ -0,0 +1,48 @@ +id(); + $table->string('name'); + $table->string('slug')->unique(); + $table->text('description'); + $table->string('url'); + $table->string('docs_url')->nullable(); + $table->string('logo_url')->nullable(); + $table->string('category'); + $table->text('install_command')->nullable(); + $table->text('config_example')->nullable(); + $table->string('transport')->nullable(); + $table->string('language')->nullable(); + $table->json('capabilities')->nullable(); + $table->json('tags')->nullable(); + $table->unsignedInteger('views')->default(0); + $table->unsignedInteger('clicks')->default(0); + $table->boolean('featured')->default(false); + $table->boolean('approved')->default(false); + $table->foreignId('user_id')->nullable()->constrained()->nullOnDelete(); + $table->timestamps(); + + $table->index(['category']); + $table->index(['approved', 'featured']); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('mcp_servers'); + } +}; diff --git a/database/migrations/2026_08_10_213713_create_skills_table.php b/database/migrations/2026_08_10_213713_create_skills_table.php new file mode 100644 index 0000000..b03534b --- /dev/null +++ b/database/migrations/2026_08_10_213713_create_skills_table.php @@ -0,0 +1,46 @@ +id(); + $table->string('name'); + $table->string('slug')->unique(); + $table->text('description'); + $table->string('url'); + $table->string('docs_url')->nullable(); + $table->string('logo_url')->nullable(); + $table->string('category'); + $table->text('install_instructions')->nullable(); + $table->text('example_prompt')->nullable(); + $table->string('requirements')->nullable(); + $table->json('tags')->nullable(); + $table->unsignedInteger('views')->default(0); + $table->unsignedInteger('clicks')->default(0); + $table->boolean('featured')->default(false); + $table->boolean('approved')->default(false); + $table->foreignId('user_id')->nullable()->constrained()->nullOnDelete(); + $table->timestamps(); + + $table->index(['category']); + $table->index(['approved', 'featured']); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('skills'); + } +}; diff --git a/resources/docs/api.md b/resources/docs/api.md index eb3ae96..66c94c6 100644 --- a/resources/docs/api.md +++ b/resources/docs/api.md @@ -217,6 +217,122 @@ curl -X POST {{BASE_URL}}/api/tools \ Same fields as create, all optional. +## MCP Servers + +A directory of Model Context Protocol servers shown at `/mcp-servers` on the public site. Fields: `id`, `name`, `slug`, `description`, `url`, `docs_url`, `logo_url`, `category`, `category_label`, `install_command`, `config_example`, `transport`, `transport_label`, `language`, `capabilities` (array of strings), `tags` (array of strings), `views`, `clicks`, `featured`, `approved`, `submitted_by`, `created_at`, `updated_at`. + +**An MCP server is only visible on the public site once `approved` is `true`.** New servers default to `approved: false` unless you explicitly set `approved: true` in the create request — this is a moderation gate, use it deliberately. + +### `GET /api/mcp-servers` + +Query params (all optional): `category` (one of the keys below), `approved` (`true`/`false`). + +Valid `category` values: `official`, `filesystem`, `databases`, `developer-tools`, `productivity`, `search`, `browser-automation`, `communication`, `data-analytics`, `other`. + +### `POST /api/mcp-servers` + +| Field | Type | Required | Notes | +|---|---|---|---| +| `name` | string | yes | max 255 | +| `slug` | string | no | must be unique; auto-generated from `name` if omitted | +| `description` | string | yes | | +| `url` | string (URL) | yes | link to the repository or homepage | +| `docs_url` | string (URL) | no | dedicated documentation link, if different from `url` | +| `logo_url` | string (URL) | no | | +| `category` | string | yes | one of the category keys above | +| `install_command` | string | no | e.g. `"npx -y @modelcontextprotocol/server-filesystem /path"` | +| `config_example` | string | no | a config snippet for an MCP client (e.g. the `mcpServers` JSON block) | +| `transport` | string | no | one of `stdio`, `sse`, `http` | +| `language` | string | no | e.g. `"TypeScript"`, `"Python"` | +| `capabilities` | array\ | no | short bullet points of what it exposes, e.g. `["Read files", "Write files"]` | +| `tags` | array\ | no | e.g. `["official", "read-only"]` | +| `featured` | boolean | no | defaults to `false` | +| `approved` | boolean | no | defaults to `false` — see note above | + +**Submit an MCP server listing:** + +```bash +curl -X POST {{BASE_URL}}/api/mcp-servers \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/json" -H "Accept: application/json" \ + -d '{ + "name": "Example Filesystem Server", + "description": "Gives an MCP client read/write access to a local directory.", + "url": "https://github.com/example/example-mcp-filesystem", + "docs_url": "https://github.com/example/example-mcp-filesystem#readme", + "category": "filesystem", + "install_command": "npx -y @example/mcp-filesystem /path/to/dir", + "config_example": "{\n \"mcpServers\": {\n \"filesystem\": {\n \"command\": \"npx\",\n \"args\": [\"-y\", \"@example/mcp-filesystem\", \"/path/to/dir\"]\n }\n }\n}", + "transport": "stdio", + "language": "TypeScript", + "capabilities": ["Read files", "Write files", "List directories"], + "tags": ["official", "filesystem"], + "approved": true + }' +``` + +`views` and `clicks` are tracked automatically by the public site and cannot be set via the API. + +### `GET /api/mcp-servers/{id}` · `PUT/PATCH /api/mcp-servers/{id}` · `DELETE /api/mcp-servers/{id}` + +Same fields as create, all optional. + +## Claude Skills + +A directory of Claude Skills shown at `/skills` on the public site. Fields: `id`, `name`, `slug`, `description`, `url`, `docs_url`, `logo_url`, `category`, `category_label`, `install_instructions`, `example_prompt`, `requirements`, `tags` (array of strings), `views`, `clicks`, `featured`, `approved`, `submitted_by`, `created_at`, `updated_at`. + +**A skill is only visible on the public site once `approved` is `true`.** New skills default to `approved: false` unless you explicitly set `approved: true` in the create request — this is a moderation gate, use it deliberately. + +### `GET /api/skills` + +Query params (all optional): `category` (one of the keys below), `approved` (`true`/`false`). + +Valid `category` values: `document-processing`, `development`, `design`, `productivity`, `data-analysis`, `communication`, `other`. + +### `POST /api/skills` + +| Field | Type | Required | Notes | +|---|---|---|---| +| `name` | string | yes | max 255 | +| `slug` | string | no | must be unique; auto-generated from `name` if omitted | +| `description` | string | yes | | +| `url` | string (URL) | yes | link to the repository or homepage | +| `docs_url` | string (URL) | no | dedicated documentation link, if different from `url` | +| `logo_url` | string (URL) | no | | +| `category` | string | yes | one of the category keys above | +| `install_instructions` | string | no | e.g. `"Copy into ~/.claude/skills/ and restart Claude"` | +| `example_prompt` | string | no | a sample prompt that would trigger/use this skill | +| `requirements` | string | no | e.g. `"Claude Code"`, `"Claude with Code Execution tool"` | +| `tags` | array\ | no | e.g. `["official", "anthropic"]` | +| `featured` | boolean | no | defaults to `false` | +| `approved` | boolean | no | defaults to `false` — see note above | + +**Submit a skill listing:** + +```bash +curl -X POST {{BASE_URL}}/api/skills \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/json" -H "Accept: application/json" \ + -d '{ + "name": "Example PDF Skill", + "description": "Extracts text and tables from PDF files and can fill out PDF forms.", + "url": "https://github.com/example/example-pdf-skill", + "docs_url": "https://github.com/example/example-pdf-skill#readme", + "category": "document-processing", + "install_instructions": "Copy this folder into ~/.claude/skills/pdf/ and restart Claude.", + "example_prompt": "Extract the line items from this invoice PDF into a table.", + "requirements": "Claude Code", + "tags": ["official", "pdf"], + "approved": true + }' +``` + +`views` and `clicks` are tracked automatically by the public site and cannot be set via the API. + +### `GET /api/skills/{id}` · `PUT/PATCH /api/skills/{id}` · `DELETE /api/skills/{id}` + +Same fields as create, all optional. + ## End-to-end example: publish a post as an agent ```bash diff --git a/resources/views/components/layouts/app.blade.php b/resources/views/components/layouts/app.blade.php index 6854ee4..140cf0c 100644 --- a/resources/views/components/layouts/app.blade.php +++ b/resources/views/components/layouts/app.blade.php @@ -53,6 +53,8 @@ diff --git a/resources/views/components/mcp-server-card.blade.php b/resources/views/components/mcp-server-card.blade.php new file mode 100644 index 0000000..76e5be0 --- /dev/null +++ b/resources/views/components/mcp-server-card.blade.php @@ -0,0 +1,32 @@ + diff --git a/resources/views/components/skill-card.blade.php b/resources/views/components/skill-card.blade.php new file mode 100644 index 0000000..38ae610 --- /dev/null +++ b/resources/views/components/skill-card.blade.php @@ -0,0 +1,32 @@ + diff --git a/resources/views/mcp-servers/category.blade.php b/resources/views/mcp-servers/category.blade.php new file mode 100644 index 0000000..281c728 --- /dev/null +++ b/resources/views/mcp-servers/category.blade.php @@ -0,0 +1,47 @@ + + @push('jsonld') + + @endpush + +

+ MCP Servers + + {{ $label }} +

+

{{ $label }} MCP Servers

+ + + +
+ @forelse ($servers as $server) + + @empty +

No MCP servers in this category yet.

+ @endforelse +
+ +
+ {{ $servers->links() }} +
+
diff --git a/resources/views/mcp-servers/index.blade.php b/resources/views/mcp-servers/index.blade.php new file mode 100644 index 0000000..cc40387 --- /dev/null +++ b/resources/views/mcp-servers/index.blade.php @@ -0,0 +1,54 @@ + + @push('jsonld') + + @endpush + +

MCP Servers Directory

+

A curated list of Model Context Protocol servers, with docs, install instructions, and config examples.

+ +
+ +
+ + + +
+ @forelse ($servers as $server) + + @empty +

No MCP servers found.

+ @endforelse +
+ +
+ {{ $servers->links() }} +
+
diff --git a/resources/views/mcp-servers/show.blade.php b/resources/views/mcp-servers/show.blade.php new file mode 100644 index 0000000..2247ac8 --- /dev/null +++ b/resources/views/mcp-servers/show.blade.php @@ -0,0 +1,103 @@ + + @push('jsonld') + + @endpush + +

+ MCP Servers + + {{ $server->category_label }} +

+ +
+ @if ($server->logo_url) + {{ $server->name }} logo + @endif +
+

{{ $server->name }}

+

+ {{ $server->category_label }} + @if ($server->language) · {{ $server->language }} @endif + @if ($server->transport_label) · {{ $server->transport_label }} @endif +

+
+
+ +
+ + View repository → + + @if ($server->docs_url) + + Documentation → + + @endif +
+ +

{{ $server->description }}

+ + @if ($server->install_command) +
+

Install

+
{{ $server->install_command }}
+
+ @endif + + @if ($server->config_example) +
+

Example configuration

+
{{ $server->config_example }}
+
+ @endif + + @if (! empty($server->capabilities)) +
+

Capabilities

+
    + @foreach ($server->capabilities as $capability) +
  • {{ $capability }}
  • + @endforeach +
+
+ @endif + + @if (! empty($server->tags)) +
+ @foreach ($server->tags as $tag) + #{{ $tag }} + @endforeach +
+ @endif + + @if ($related->isNotEmpty()) +
+

More {{ $server->category_label }} servers

+
+ @foreach ($related as $relatedServer) + + @endforeach +
+
+ @endif +
diff --git a/resources/views/sitemap.blade.php b/resources/views/sitemap.blade.php index be51371..48d7113 100644 --- a/resources/views/sitemap.blade.php +++ b/resources/views/sitemap.blade.php @@ -10,6 +10,16 @@ daily 1.0 + + {{ route('mcp-servers.index') }} + daily + 1.0 + + + {{ route('skills.index') }} + daily + 1.0 + @foreach ($categories as $category) {{ route('blog.category', $category) }} @@ -49,6 +59,36 @@ 0.7 @endforeach + @foreach ($mcpServerCategories as $mcpServerCategory) + + {{ route('mcp-servers.category', $mcpServerCategory) }} + weekly + 0.6 + + @endforeach + @foreach ($mcpServers as $server) + + {{ route('mcp-servers.show', $server) }} + {{ $server->updated_at->toAtomString() }} + monthly + 0.7 + + @endforeach + @foreach ($skillCategories as $skillCategory) + + {{ route('skills.category', $skillCategory) }} + weekly + 0.6 + + @endforeach + @foreach ($skills as $skill) + + {{ route('skills.show', $skill) }} + {{ $skill->updated_at->toAtomString() }} + monthly + 0.7 + + @endforeach @foreach ($pages as $page) {{ route('pages.show', $page) }} diff --git a/resources/views/skills/category.blade.php b/resources/views/skills/category.blade.php new file mode 100644 index 0000000..c4d7058 --- /dev/null +++ b/resources/views/skills/category.blade.php @@ -0,0 +1,47 @@ + + @push('jsonld') + + @endpush + +

+ Claude Skills + + {{ $label }} +

+

{{ $label }} Claude Skills

+ + + +
+ @forelse ($skills as $skill) + + @empty +

No skills in this category yet.

+ @endforelse +
+ +
+ {{ $skills->links() }} +
+
diff --git a/resources/views/skills/index.blade.php b/resources/views/skills/index.blade.php new file mode 100644 index 0000000..b792cfd --- /dev/null +++ b/resources/views/skills/index.blade.php @@ -0,0 +1,54 @@ + + @push('jsonld') + + @endpush + +

Claude Skills Directory

+

A curated list of Claude Skills, with docs, install instructions, and example prompts.

+ +
+ +
+ + + +
+ @forelse ($skills as $skill) + + @empty +

No skills found.

+ @endforelse +
+ +
+ {{ $skills->links() }} +
+
diff --git a/resources/views/skills/show.blade.php b/resources/views/skills/show.blade.php new file mode 100644 index 0000000..5515ad2 --- /dev/null +++ b/resources/views/skills/show.blade.php @@ -0,0 +1,90 @@ + + @push('jsonld') + + @endpush + +

+ Claude Skills + + {{ $skill->category_label }} +

+ +
+ @if ($skill->logo_url) + {{ $skill->name }} logo + @endif +
+

{{ $skill->name }}

+

+ {{ $skill->category_label }} + @if ($skill->requirements) · {{ $skill->requirements }} @endif +

+
+
+ +
+ + View repository → + + @if ($skill->docs_url) + + Documentation → + + @endif +
+ +

{{ $skill->description }}

+ + @if ($skill->install_instructions) +
+

How to install

+

{{ $skill->install_instructions }}

+
+ @endif + + @if ($skill->example_prompt) +
+

Example prompt

+
{{ $skill->example_prompt }}
+
+ @endif + + @if (! empty($skill->tags)) +
+ @foreach ($skill->tags as $tag) + #{{ $tag }} + @endforeach +
+ @endif + + @if ($related->isNotEmpty()) +
+

More {{ $skill->category_label }} skills

+
+ @foreach ($related as $relatedSkill) + + @endforeach +
+
+ @endif +
diff --git a/routes/api.php b/routes/api.php index 061e538..419f87d 100644 --- a/routes/api.php +++ b/routes/api.php @@ -2,7 +2,9 @@ use App\Http\Controllers\Api\AuthController; use App\Http\Controllers\Api\CategoryController; +use App\Http\Controllers\Api\McpServerController; use App\Http\Controllers\Api\PostController; +use App\Http\Controllers\Api\SkillController; use App\Http\Controllers\Api\TagController; use App\Http\Controllers\Api\ToolController; use Illuminate\Http\Request; @@ -18,4 +20,6 @@ Route::middleware('auth:sanctum')->group(function () { Route::apiResource('categories', CategoryController::class)->scoped(['category' => 'id'])->names('api.categories'); Route::apiResource('tags', TagController::class)->scoped(['tag' => 'id'])->names('api.tags'); Route::apiResource('tools', ToolController::class)->scoped(['tool' => 'id'])->names('api.tools'); + Route::apiResource('mcp-servers', McpServerController::class)->scoped(['mcp_server' => 'id'])->names('api.mcp_servers'); + Route::apiResource('skills', SkillController::class)->scoped(['skill' => 'id'])->names('api.skills'); }); diff --git a/routes/web.php b/routes/web.php index 20168d0..d39645a 100644 --- a/routes/web.php +++ b/routes/web.php @@ -2,8 +2,10 @@ use App\Http\Controllers\ApiDocsController; use App\Http\Controllers\BlogController; +use App\Http\Controllers\McpServerController; use App\Http\Controllers\PageController; use App\Http\Controllers\SitemapController; +use App\Http\Controllers\SkillController; use App\Http\Controllers\ToolController; use Illuminate\Support\Facades\Route; @@ -23,6 +25,16 @@ Route::get('/tools/category/{category}', [ToolController::class, 'category'])->n Route::get('/tools/{tool:slug}/visit', [ToolController::class, 'visit'])->name('tools.visit'); Route::get('/tools/{tool:slug}', [ToolController::class, 'show'])->name('tools.show'); +Route::get('/mcp-servers', [McpServerController::class, 'index'])->name('mcp-servers.index'); +Route::get('/mcp-servers/category/{category}', [McpServerController::class, 'category'])->name('mcp-servers.category'); +Route::get('/mcp-servers/{mcpServer:slug}/visit', [McpServerController::class, 'visit'])->name('mcp-servers.visit'); +Route::get('/mcp-servers/{mcpServer:slug}', [McpServerController::class, 'show'])->name('mcp-servers.show'); + +Route::get('/skills', [SkillController::class, 'index'])->name('skills.index'); +Route::get('/skills/category/{category}', [SkillController::class, 'category'])->name('skills.category'); +Route::get('/skills/{skill:slug}/visit', [SkillController::class, 'visit'])->name('skills.visit'); +Route::get('/skills/{skill:slug}', [SkillController::class, 'show'])->name('skills.show'); + // Kept last: a single-segment catch-all for static pages (e.g. /about). // Must not precede any other single-segment route above (e.g. /tools, /login). Route::get('/{page:slug}', [PageController::class, 'show'])->name('pages.show');