Skip to content
Merged
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
6 changes: 0 additions & 6 deletions apps/mesh/src/web/components/store/app-detail/index.ts

This file was deleted.

10 changes: 7 additions & 3 deletions apps/mesh/src/web/components/store/index.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
/**
* Store Components
*
* Components for store discovery and item browsing.
* Components for store discovery and MCP Server browsing.
*/

// Main components
export { StoreDiscovery } from "./store-discovery";

// Item components
// Types
export type {
MCPRegistryServer,
MCPRegistryServerIcon,
MCPRegistryServerMeta,
} from "./registry-item-card";
RegistryItem,
FilterItem,
RegistryFiltersResponse,
ActiveFilters,
} from "./types";
Original file line number Diff line number Diff line change
Expand Up @@ -8,74 +8,20 @@ import { Card } from "@deco/ui/components/card.js";
import { IntegrationIcon } from "../integration-icon.tsx";
import { getGitHubAvatarUrl } from "@/web/utils/github-icon";
import { extractDisplayNameFromDomain } from "@/web/utils/app-name";
import type { RegistryItem } from "./registry-items-section";
import type { RegistryItem } from "./types";

/**
* MCP Registry Server structure from LIST response
*/
export interface MCPRegistryServerIcon {
src: string;
mimeType?: string;
sizes?: string[];
theme?: "light" | "dark";
}

export interface MCPRegistryServerMeta {
"mcp.mesh"?: {
id: string;
verified?: boolean;
scopeName?: string;
appName?: string;
publishedAt?: string;
updatedAt?: string;
};
"mcp.mesh/publisher-provided"?: {
friendlyName?: string | null;
metadata?: Record<string, unknown> | null;
tools?: Array<{
id: string;
name: string;
description?: string | null;
}>;
models?: unknown[];
emails?: unknown[];
analytics?: unknown;
cdn?: unknown;
};
[key: string]: unknown;
}

export interface MCPRegistryServer {
id: string;
title: string;
created_at: string;
updated_at: string;
_meta?: MCPRegistryServerMeta;
server: {
$schema?: string;
_meta?: MCPRegistryServerMeta;
name: string;
title?: string;
description?: string;
icons?: MCPRegistryServerIcon[];
remotes?: Array<{
type: "http" | "stdio" | "sse";
url?: string;
}>;
version?: string;
repository?: {
url?: string;
source?: string;
subfolder?: string;
};
};
}
// Re-export types for backwards compatibility
export type {
MCPRegistryServer,
MCPRegistryServerIcon,
MCPRegistryServerMeta,
RegistryItem,
} from "./types";

/**
* Simplified props for RegistryItemCard - receives processed data
* Reduces component responsibility to just rendering
* Props for MCPServerCard - receives processed data
*/
interface RegistryItemCardProps {
interface MCPServerCardProps {
icon: string | null;
scopeName: string | null;
displayName: string;
Expand All @@ -90,16 +36,24 @@ interface RegistryItemCardProps {
* Extract display data from a registry item for the card component
* Handles name parsing, icon extraction, and verification status
*/
export function extractCardDisplayData(
function extractCardDisplayData(
item: RegistryItem,
): Omit<RegistryItemCardProps, "onClick"> {
): Omit<MCPServerCardProps, "onClick"> {
const rawTitle = item.title || item.server.title || item.id || "Unnamed Item";
const description = item.server.description || null;
const meshMeta = item._meta?.["mcp.mesh"];

// Description priority: short_description > mesh_description > server.description
const description =
meshMeta?.short_description ||
meshMeta?.mesh_description ||
item.server.description ||
null;

const icon =
item.server.icons?.[0]?.src ||
getGitHubAvatarUrl(item.server.repository) ||
null;
const isVerified = item._meta?.["mcp.mesh"]?.verified ?? false;
const isVerified = meshMeta?.verified ?? false;
const version = item.server.version;
const hasRemotes = (item.server.remotes?.length ?? 0) > 0;
const canInstall = hasRemotes;
Expand All @@ -119,15 +73,20 @@ export function extractCardDisplayData(

// Fallback to _meta if scopeName wasn't extracted from title
if (!scopeName) {
const metaScopeName = item._meta?.["mcp.mesh"]?.scopeName;
const metaAppName = item._meta?.["mcp.mesh"]?.appName;
const metaScopeName = meshMeta?.scopeName;
const metaAppName = meshMeta?.appName;
if (metaScopeName && metaAppName) {
scopeName = `${metaScopeName}/${metaAppName}`;
} else if (metaScopeName) {
scopeName = metaScopeName;
}
}

// PRIORITY: Use friendly_name if available, otherwise use displayName
if (meshMeta?.friendly_name) {
displayName = meshMeta.friendly_name;
}

return {
icon,
scopeName,
Expand All @@ -139,7 +98,10 @@ export function extractCardDisplayData(
};
}

export function RegistryItemCard({
/**
* Card component for displaying an MCP Server in the store grid
*/
function MCPServerCard({
icon,
scopeName,
displayName,
Expand All @@ -148,7 +110,7 @@ export function RegistryItemCard({
isVerified,
canInstall,
onClick,
}: RegistryItemCardProps) {
}: MCPServerCardProps) {
return (
<Card
className="p-6 cursor-pointer hover:bg-muted/50 transition-colors"
Expand Down Expand Up @@ -209,3 +171,47 @@ export function RegistryItemCard({
</Card>
);
}

/**
* Props for MCPServerCardGrid
*/
interface MCPServerCardGridProps {
items: RegistryItem[];
title: string;
subtitle?: string;
onItemClick: (item: RegistryItem) => void;
totalCount?: number | null;
}

/**
* Grid component for displaying multiple MCP Server cards
*/
export function MCPServerCardGrid({
items,
title,
onItemClick,
}: MCPServerCardGridProps) {
if (items.length === 0) return null;

return (
<div className="flex flex-col gap-4">
{title && (
<div className="flex items-center justify-between w-max gap-2">
<h2 className="text-lg font-medium">{title}</h2>
</div>
)}
<div className="grid grid-cols-4 gap-4">
{items.map((item) => {
const displayData = extractCardDisplayData(item);
return (
<MCPServerCard
key={item.id}
{...displayData}
onClick={() => onItemClick(item)}
/>
);
})}
</div>
</div>
);
}
6 changes: 6 additions & 0 deletions apps/mesh/src/web/components/store/mcp-server-detail/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export * from "./types";
export * from "./mcp-server-detail-states";
export * from "./mcp-server-detail-header";
export * from "./mcp-server-hero-section";
export * from "./mcp-server-detail-sidebar";
export * from "./mcp-server-tabs-content";
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { Button } from "@deco/ui/components/button.tsx";
import { ArrowLeft } from "@untitledui/icons";

interface AppDetailHeaderProps {
interface MCPServerDetailHeaderProps {
onBack: () => void;
}

export function AppDetailHeader({ onBack }: AppDetailHeaderProps) {
export function MCPServerDetailHeader({ onBack }: MCPServerDetailHeaderProps) {
return (
<div className="flex items-center h-12 border-b border-border shrink-0">
{/* Back Button */}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { RegistryItem } from "@/web/components/store/registry-items-section";
import type { RegistryItem } from "@/web/components/store/types";
import { IntegrationIcon } from "@/web/components/integration-icon.tsx";
import { LinkExternal01 } from "@untitledui/icons";
import type { AppData, PublisherInfo } from "./types";
import type { MCPServerData, PublisherInfo } from "./types";

/** Format date to MMM DD, YYYY format */
function formatLastUpdated(date: unknown): string {
Expand All @@ -19,17 +19,17 @@ function formatLastUpdated(date: unknown): string {
}
}

interface AppSidebarProps {
data: AppData;
interface MCPServerDetailSidebarProps {
data: MCPServerData;
publisherInfo: PublisherInfo;
selectedItem: RegistryItem;
}

export function AppSidebar({
export function MCPServerDetailSidebar({
data,
publisherInfo,
selectedItem,
}: AppSidebarProps) {
}: MCPServerDetailSidebarProps) {
return (
<div className="lg:col-span-1 flex flex-col pt-5">
{/* Overview */}
Expand Down Expand Up @@ -69,8 +69,8 @@ export function AppSidebar({
<span>
{publisherInfo.count}{" "}
{publisherInfo.count === 1
? "published app"
: "published apps"}
? "published server"
: "published servers"}
</span>
</>
) : (
Expand All @@ -92,6 +92,38 @@ export function AppSidebar({
</div>
)}

{data.tags && data.tags.length > 0 && (
<div className="flex justify-between items-start text-sm">
<span className="text-foreground text-sm">Tags</span>
<div className="flex flex-wrap gap-1 justify-end max-w-[60%]">
{data.tags.map((tag) => (
<span
key={tag}
className="text-xs bg-muted px-2 py-0.5 rounded"
>
{tag}
</span>
))}
</div>
</div>
)}

{data.categories && data.categories.length > 0 && (
<div className="flex justify-between items-start text-sm">
<span className="text-foreground text-sm">Categories</span>
<div className="flex flex-wrap gap-1 justify-end max-w-[60%]">
{data.categories.map((cat) => (
<span
key={cat}
className="text-xs bg-muted px-2 py-0.5 rounded"
>
{cat}
</span>
))}
</div>
</div>
)}

{data.connectionType && (
<div className="flex justify-between items-center text-sm">
<span className="text-foreground text-sm">Connection Type</span>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ interface LoadingStateProps {
message?: string;
}

export function AppDetailLoadingState({
message = "Loading app details...",
export function MCPServerDetailLoadingState({
message = "Loading MCP Server details...",
}: LoadingStateProps) {
return (
<div className="flex flex-col items-center justify-center h-full">
Expand All @@ -20,13 +20,13 @@ interface ErrorStateProps {
onBack: () => void;
}

export function AppDetailErrorState({ error, onBack }: ErrorStateProps) {
export function MCPServerDetailErrorState({ error, onBack }: ErrorStateProps) {
const errorMessage = error instanceof Error ? error.message : error;

return (
<div className="flex flex-col items-center justify-center h-full">
<AlertCircle size={48} className="text-destructive mb-4" />
<h3 className="text-lg font-medium mb-2">Error loading app</h3>
<h3 className="text-lg font-medium mb-2">Error loading MCP Server</h3>
<p className="text-muted-foreground max-w-md text-center">
{errorMessage}
</p>
Expand All @@ -44,13 +44,13 @@ interface NotFoundStateProps {
onBack: () => void;
}

export function AppDetailNotFoundState({ onBack }: NotFoundStateProps) {
export function MCPServerDetailNotFoundState({ onBack }: NotFoundStateProps) {
return (
<div className="flex flex-col items-center justify-center h-full">
<SearchLg size={48} className="text-muted-foreground mb-4" />
<h3 className="text-lg font-medium mb-2">App not found</h3>
<h3 className="text-lg font-medium mb-2">MCP Server not found</h3>
<p className="text-muted-foreground max-w-md text-center">
The app you're looking for doesn't exist in this store.
The MCP Server you're looking for doesn't exist in this store.
</p>
<button
onClick={onBack}
Expand Down
Loading