Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 105 additions & 0 deletions app/app/mcp-catalog/apps/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { Metadata } from 'next';
import { Suspense } from 'react';

import Footer from '@components/Footer';
import Header from '@components/Header';
import McpAppsClient from '@mcpCatalog/components/McpAppsClient';
import { getAppCategories, getAppPricingOptions, loadApps } from '@mcpCatalog/lib/apps';

export const metadata: Metadata = {
title: 'MCP Apps Catalog | Browse MCP Client Applications',
description:
'Explore the catalog of applications that support the Model Context Protocol (MCP) as clients. Find IDEs, desktop apps, automation tools, and frameworks that connect to MCP servers.',
keywords: ['MCP apps', 'MCP clients', 'Model Context Protocol apps', 'Claude Desktop', 'Cursor', 'MCP IDEs'],
openGraph: {
title: 'MCP Apps Catalog | Browse MCP Client Applications',
description:
'Explore apps that consume MCP servers – IDEs, desktop clients, automation platforms, and AI frameworks with native MCP support.',
type: 'website',
},
twitter: {
card: 'summary_large_image',
title: 'MCP Apps Catalog | Browse MCP Client Applications',
description:
'Explore apps that consume MCP servers – IDEs, desktop clients, automation platforms, and AI frameworks with native MCP support.',
},
};

// Force dynamic rendering
export const dynamic = 'force-dynamic';

export default function McpAppsPage() {
const apps = loadApps();
const categories = getAppCategories(apps);
const pricingOptions = getAppPricingOptions(apps);

return (
<div className="min-h-screen flex flex-col">
<Header />

<main className="flex-1 relative flex flex-col">
<div
className="absolute inset-0 z-0"
style={{
backgroundImage:
'linear-gradient(to right, #f0f0f0 1px, transparent 1px), linear-gradient(to bottom, #f0f0f0 1px, transparent 1px)',
backgroundSize: '40px 40px',
}}
/>

<div className="container relative z-10 px-4 sm:px-6 lg:px-8 py-8 sm:py-12 lg:py-16">
{/* Page Header */}
<div className="mb-10">
<div className="flex items-center gap-3 mb-3">
<h1 className="text-3xl sm:text-4xl lg:text-5xl font-bold text-gray-900">
MCP Apps <span className="text-2xl sm:text-3xl lg:text-4xl text-gray-600">Catalog</span>
</h1>
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-amber-100 text-amber-800 border border-amber-200">
Beta
</span>
</div>

<p className="text-base sm:text-lg text-gray-600 mb-6 max-w-2xl">
{apps.length} applications that consume MCP servers — IDEs, desktop clients, automation platforms, and AI
frameworks with native{' '}
<a
href="https://modelcontextprotocol.io"
className="text-blue-600 hover:underline"
target="_blank"
rel="noopener noreferrer"
>
Model Context Protocol
</a>{' '}
support.
</p>

{/* Breadcrumb navigation */}
<nav className="flex items-center gap-2 text-sm text-gray-500 mb-6">
<a href="/mcp-catalog" className="hover:text-gray-900 transition-colors">
MCP Catalog
</a>
<span>/</span>
<span className="text-gray-900 font-medium">MCP Apps</span>
</nav>

{/* Info banner */}
<div className="bg-purple-50 border border-purple-200 rounded-lg p-4 max-w-2xl">
<p className="text-sm text-purple-800">
<span className="font-semibold">What is an MCP App?</span> An MCP app is a client application that
connects to MCP servers to extend its AI capabilities. Unlike MCP servers (which provide tools and
context), MCP apps <em>consume</em> those servers — enabling your AI assistant, IDE, or automation
platform to access real-world data and actions.
</p>
</div>
</div>

<Suspense fallback={<div>Loading apps...</div>}>
<McpAppsClient apps={apps} categories={categories} pricingOptions={pricingOptions} />
</Suspense>
</div>
</main>

<Footer />
</div>
);
}
142 changes: 142 additions & 0 deletions app/app/mcp-catalog/components/McpAppCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import Image from 'next/image';

import { ArchestraMcpApp } from '@mcpCatalog/types';

const PLATFORM_LABELS: Record<string, string> = {
windows: 'Windows',
mac: 'macOS',
linux: 'Linux',
web: 'Web',
};

const PRICING_STYLES: Record<string, string> = {
free: 'bg-green-100 text-green-800 border-green-200',
freemium: 'bg-blue-100 text-blue-800 border-blue-200',
paid: 'bg-orange-100 text-orange-800 border-orange-200',
};

const PRICING_LABELS: Record<string, string> = {
free: 'Free',
freemium: 'Freemium',
paid: 'Paid',
};

type MpcFeatureKey = 'tools' | 'resources' | 'prompts' | 'sampling' | 'stdio_transport' | 'http_transport';

const MCP_FEATURE_ENTRIES: { key: MpcFeatureKey; label: string }[] = [
{ key: 'tools', label: 'Tools' },
{ key: 'resources', label: 'Resources' },
{ key: 'prompts', label: 'Prompts' },
{ key: 'sampling', label: 'Sampling' },
{ key: 'stdio_transport', label: 'STDIO' },
{ key: 'http_transport', label: 'HTTP' },
];

function FeaturePill({ active, label }: { active: boolean; label: string }) {
const activeClass = 'bg-purple-50 text-purple-700 border-purple-200';
const inactiveClass = 'bg-gray-50 text-gray-400 border-gray-200 line-through';
return (
<span
className={`inline-flex items-center px-2 py-0.5 rounded text-xs font-medium border ${active ? activeClass : inactiveClass}`}
>
{label}
</span>
);
}

export default function McpAppCard({ app }: { app: ArchestraMcpApp }) {
const pricingClass = PRICING_STYLES[app.pricing] ?? PRICING_STYLES.free;

return (
<div className="bg-white rounded-xl border border-gray-200 shadow-sm hover:shadow-md transition-shadow flex flex-col p-5 gap-4">
{/* Header */}
<div className="flex items-start gap-3">
<div className="w-12 h-12 flex-shrink-0 rounded-lg overflow-hidden bg-gray-50 border border-gray-100 flex items-center justify-center">
<Image
src={app.logo_url}
alt={`${app.display_name} logo`}
width={48}
height={48}
className="object-contain"
unoptimized
/>
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 flex-wrap">
<h3 className="text-base font-semibold text-gray-900 truncate">{app.display_name}</h3>
{app.open_source && (
<span className="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-emerald-50 text-emerald-700 border border-emerald-200">
Open Source
</span>
)}
</div>
<div className="flex items-center gap-2 mt-1 flex-wrap">
<span className="text-xs text-gray-500">{app.category}</span>
<span
className={`inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium border ${pricingClass}`}
>
{PRICING_LABELS[app.pricing]}
</span>
</div>
</div>
</div>

{/* Description */}
<p className="text-sm text-gray-600 leading-relaxed line-clamp-3">{app.description}</p>

{/* Supported Platforms */}
<div className="flex flex-wrap gap-1">
{app.supported_platforms.map((platform) => (
<span
key={platform}
className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-gray-100 text-gray-600 border border-gray-200"
>
{PLATFORM_LABELS[platform] ?? platform}
</span>
))}
</div>

{/* MCP Features */}
<div>
<p className="text-xs font-medium text-gray-500 uppercase tracking-wide mb-1.5">MCP Features</p>
<div className="flex flex-wrap gap-1">
{MCP_FEATURE_ENTRIES.map(({ key, label }) => (
<FeaturePill key={key} active={app.mcp_features[key]} label={label} />
))}
</div>
</div>

{/* Footer Links */}
<div className="flex items-center gap-3 mt-auto pt-2 border-t border-gray-100">
<a
href={app.website_url}
target="_blank"
rel="noopener noreferrer"
className="text-sm font-medium text-purple-600 hover:text-purple-800 transition-colors"
>
Website ↗
</a>
{app.mcp_docs_url && (
<a
href={app.mcp_docs_url}
target="_blank"
rel="noopener noreferrer"
className="text-sm font-medium text-gray-500 hover:text-gray-700 transition-colors"
>
MCP Docs ↗
</a>
)}
{app.github_url && (
<a
href={app.github_url}
target="_blank"
rel="noopener noreferrer"
className="text-sm font-medium text-gray-500 hover:text-gray-700 transition-colors ml-auto"
>
GitHub ↗
</a>
)}
</div>
</div>
);
}
117 changes: 117 additions & 0 deletions app/app/mcp-catalog/components/McpAppsClient.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
'use client';

import { useMemo, useState } from 'react';

import McpAppCard from '@mcpCatalog/components/McpAppCard';
import { ArchestraMcpApp } from '@mcpCatalog/types';

interface McpAppsClientProps {
apps: ArchestraMcpApp[];
categories: string[];
pricingOptions: string[];
}

export default function McpAppsClient({ apps, categories, pricingOptions }: McpAppsClientProps) {
const [search, setSearch] = useState('');
const [selectedCategory, setSelectedCategory] = useState('All');
const [selectedPricing, setSelectedPricing] = useState('All');
const [openSourceOnly, setOpenSourceOnly] = useState(false);

const filtered = useMemo(() => {
return apps.filter((app) => {
const q = search.toLowerCase();
if (q && !app.display_name.toLowerCase().includes(q) && !app.description.toLowerCase().includes(q)) {
return false;
}
if (selectedCategory !== 'All' && app.category !== selectedCategory) {
return false;
}
if (selectedPricing !== 'All' && app.pricing !== selectedPricing) {
return false;
}
if (openSourceOnly && !app.open_source) {
return false;
}
return true;
});
}, [apps, search, selectedCategory, selectedPricing, openSourceOnly]);

const PRICING_LABELS: Record<string, string> = {
free: 'Free',
freemium: 'Freemium',
paid: 'Paid',
};

return (
<div className="space-y-6">
{/* Filters */}
<div className="bg-white border border-gray-200 rounded-xl p-4 flex flex-col sm:flex-row gap-4 flex-wrap">
{/* Search */}
<input
type="text"
placeholder="Search MCP apps..."
value={search}
onChange={(e) => setSearch(e.target.value)}
className="flex-1 min-w-[180px] px-3 py-2 text-sm border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent"
/>

{/* Category filter */}
<select
value={selectedCategory}
onChange={(e) => setSelectedCategory(e.target.value)}
className="px-3 py-2 text-sm border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-500 bg-white"
>
{categories.map((cat) => (
<option key={cat} value={cat}>
{cat === 'All' ? 'All Categories' : cat}
</option>
))}
</select>

{/* Pricing filter */}
<select
value={selectedPricing}
onChange={(e) => setSelectedPricing(e.target.value)}
className="px-3 py-2 text-sm border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-500 bg-white"
>
{pricingOptions.map((p) => (
<option key={p} value={p}>
{p === 'All' ? 'All Pricing' : (PRICING_LABELS[p] ?? p)}
</option>
))}
</select>

{/* Open Source toggle */}
<label className="flex items-center gap-2 cursor-pointer text-sm text-gray-700 font-medium">
<input
type="checkbox"
checked={openSourceOnly}
onChange={(e) => setOpenSourceOnly(e.target.checked)}
className="w-4 h-4 rounded border-gray-300 text-purple-600 focus:ring-purple-500"
/>
Open Source Only
</label>
</div>

{/* Result count */}
<p className="text-sm text-gray-500">
Showing <span className="font-semibold text-gray-900">{filtered.length}</span> of{' '}
<span className="font-semibold text-gray-900">{apps.length}</span> MCP apps
</p>

{/* Grid */}
{filtered.length > 0 ? (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-5">
{filtered.map((app) => (
<McpAppCard key={app.name} app={app} />
))}
</div>
) : (
<div className="text-center py-16 text-gray-400">
<p className="text-lg font-medium">No MCP apps found</p>
<p className="text-sm mt-1">Try adjusting your filters or search query</p>
</div>
)}
</div>
);
}
Loading