'use client'; import { Input } from '@/components/input'; import { Logo } from '@/components/logo'; import { MobileActionBar } from '@/components/mobile-action-bar'; import { UserMenu } from '@/components/user-menu'; import { apiFetch } from '@/lib/api'; import { cn } from '@/lib/cn'; import type { TemplateSummary } from '@/lib/templates-server'; import { Activity, GitFork, Lightbulb, ShieldCheck } from 'lucide-react'; import Link from 'next/link'; import { useEffect, useRef, useState } from 'react'; type Template = TemplateSummary; type Sort = 'trending' | 'top'; type Scope = 'all' | 'mine'; const STATUS_STYLE: Record = { public: 'border-emerald-400/40 bg-emerald-400/10 text-emerald-300', hidden: 'border-amber-400/40 bg-amber-400/10 text-amber-300', takedown: 'border-red-400/40 bg-red-400/10 text-red-300', draft: 'border-zinc-400/40 bg-zinc-400/10 text-zinc-300', }; // Honest inspiration cards for the young-marketplace empty state — clearly // labeled ideas, not fake templates with invented usage numbers. const STARTER_IDEAS: { title: string; desc: string }[] = [ { title: 'Notion workspace search', desc: 'search_pages + get_page_content over your Notion API key. One prompt, ~60s to live.', }, { title: 'Read-only PostgreSQL', desc: 'Let Claude query your tables with schema introspection — no write access.', }, { title: 'Wrap any REST API', desc: 'Turn the endpoints you already have into typed MCP tools behind OAuth.', }, ]; export function TemplatesBrowser({ initialTemplates, initialCategories, }: { initialTemplates: Template[]; initialCategories: string[]; }) { const [me, setMe] = useState<{ email: string } | null | undefined>(undefined); const [scope, setScope] = useState('all'); const [templates, setTemplates] = useState(initialTemplates); const [categories, setCategories] = useState(initialCategories); const [sort, setSort] = useState('trending'); const [category, setCategory] = useState(''); const [search, setSearch] = useState(''); // Detect login state once useEffect(() => { apiFetch<{ user: { email: string } }>('/v1/auth/me') .then((r) => setMe({ email: r.user.email })) .catch(() => setMe(null)); }, []); // Load templates whenever scope/sort/category changes. The first run is // skipped: the server component already fetched the default (all/trending) // list and passed it as props, so the grid is in the SSR HTML for crawlers. const skippedInitialLoad = useRef(false); useEffect(() => { if (!skippedInitialLoad.current) { skippedInitialLoad.current = true; return; } setTemplates(null); if (scope === 'mine') { apiFetch<{ templates: Template[]; categories: string[] }>('/v1/templates/mine') .then((r) => { setTemplates(r.templates); setCategories(r.categories); }) .catch(() => setTemplates([])); } else { const params = new URLSearchParams({ sort }); if (category) params.set('category', category); apiFetch<{ templates: Template[]; categories: string[] }>(`/v1/templates?${params}`) .then((r) => { setTemplates(r.templates); setCategories(r.categories); }) .catch(() => setTemplates([])); } }, [scope, sort, category]); const visible = templates?.filter((t) => { if (search) { const q = search.toLowerCase(); if (!t.title.toLowerCase().includes(q) && !t.shortDescription.toLowerCase().includes(q)) { return false; } } // category filter is server-side for 'all', client-side for 'mine' if (scope === 'mine' && category && t.category !== category) return false; return true; }); const loggedIn = me != null; return (
{/* "/ templates" subtitle is redundant on mobile — h1 below already names the page. Keep on desktop as breadcrumb. */} / templates
Marketplace

MCP server templates

Pre-built MCP servers from the community. Fork in one click — your own container, your own credentials, fully isolated. The template author never sees your data.

{/* Filter row: stacks vertically on mobile (search on top for thumb reach), inline on desktop. The order utility on the Input flips it to the right side at sm: while filters wrap normally. */}
setSearch(e.target.value)} placeholder="Search…" className="order-first w-full sm:order-last sm:ml-auto sm:w-60" /> {/* Chips row — horizontally scrollable on narrow mobile so the segmented controls never get squeezed below their min-width. */}
{loggedIn && ( <>
{(['all', 'mine'] as Scope[]).map((s) => ( ))}
)} {scope === 'all' && ( <>
{(['trending', 'top'] as Sort[]).map((s) => ( ))}
)} truncates the visible label with ellipsis when // the box is narrower than the text; the dropdown panel // itself still shows full names when opened. className="h-7 w-[140px] shrink-0 truncate rounded-md border border-[--color-border] bg-[--color-bg-subtle] px-2 text-[12.5px] focus:border-[--color-accent] focus:outline-none sm:w-[160px]" > {categories.map((c) => ( ))}
{!visible &&

Loading…

} {visible && visible.length === 0 && scope === 'mine' && (

You haven't published any templates.

Build a server, then tick Share as template on the done screen — or use the Publish tab on any live server.

)} {visible && visible.length === 0 && scope === 'all' && (

The marketplace is young — be one of the first to publish.

Every server you build can be shared as a forkable template (your credentials never travel with it). Not sure where to start? The{' '} guides {' '} walk through hosting and auth.

Starter ideas — build one from a single prompt

{STARTER_IDEAS.map((idea) => (

{idea.title}

{idea.desc}

Build this → ))}
)} {visible && visible.length > 0 && (
{visible.map((t) => ( ))}
)}
Every template is isolated: forking creates your own container with your own secrets.
{/* Mobile tab-bar — only when signed in. Logged-out marketplace browsing keeps the simple marketing chrome (login CTA in header). */} {loggedIn && }
); } function TemplateCard({ t, showStatus }: { t: Template; showStatus: boolean }) { // takedown templates can't be opened (the detail route 410s); link to the // server's Publish tab so the owner can see why. Everything else opens detail. const href = t.status === 'takedown' && t.sourceServerId ? `/servers/${t.sourceServerId}` : `/templates/${t.slug}`; return (

{t.title}

{t.verified && ( verified )}
{showStatus ? ( {t.status} ) : ( {t.category} )}

{t.shortDescription}

{t.forkCount} {t.activeDeployments}
{showStatus ? t.category : `by ${t.ownerName ?? t.ownerOrgName ?? 'anonymous'}`}
); }