Docs: Revamp extensions site (#1260)

Co-authored-by: Nahiyan Khan <nahiyan@squareup.com>
This commit is contained in:
Adewale Abati
2025-03-24 14:56:40 +01:00
committed by GitHub
parent eb9b7fa5ee
commit bf760fd630
56 changed files with 2019 additions and 1029 deletions
@@ -0,0 +1,138 @@
import { Star, Download, Terminal, ChevronRight, Info } from "lucide-react";
import { Badge } from "@site/src/components/ui/badge";
import { Button } from "@site/src/components/ui/button";
import type { MCPServer } from "@site/src/types/server";
import Link from "@docusaurus/Link";
import { useState } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { getGooseInstallLink } from "@site/src/utils/install-links";
export function ServerCard({ server }: { server: MCPServer }) {
const [isCommandVisible, setIsCommandVisible] = useState(false);
return (
<div className="extension-title h-full">
<div className="server-card interactive w-full h-full">
<div className="card-glow"></div>
<div className="card">
<div className="card-header">
<div className="card-header-content">
<svg
className="extension-icon"
width="13"
height="12"
viewBox="0 0 13 12"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<g id="Response Icons">
<path
id="Vector"
d="M6.29521 0.0644531C3.24885 0.0644531 3.43906 1.37819 3.43906 1.37819L3.44245 2.73927H6.34953V3.1479H2.2878C2.2878 3.1479 0.33844 2.92804 0.33844 5.98471C0.33844 9.04147 2.03989 8.93307 2.03989 8.93307H3.05531V7.51461C3.05531 7.51461 3.00057 5.82262 4.7296 5.82262H7.6129C7.6129 5.82262 9.23283 5.84864 9.23283 4.26571V1.64842C9.23283 1.64842 9.47886 0.0644531 6.29521 0.0644531ZM4.69225 0.979748C4.76094 0.979686 4.82898 0.993097 4.89245 1.01921C4.95593 1.04533 5.01361 1.08363 5.06219 1.13194C5.11076 1.18024 5.14928 1.2376 5.17554 1.30073C5.2018 1.36385 5.21529 1.43151 5.21523 1.49982C5.21529 1.56814 5.2018 1.63579 5.17554 1.69892C5.14928 1.76205 5.11076 1.8194 5.06219 1.86771C5.01361 1.91601 4.95593 1.95432 4.89245 1.98044C4.82898 2.00655 4.76094 2.01996 4.69225 2.0199C4.62355 2.01996 4.55551 2.00655 4.49204 1.98044C4.42856 1.95432 4.37088 1.91601 4.3223 1.86771C4.27373 1.8194 4.23521 1.76205 4.20895 1.69892C4.18269 1.63579 4.1692 1.56814 4.16926 1.49982C4.1692 1.43151 4.18269 1.36385 4.20895 1.30073C4.23521 1.2376 4.27373 1.18024 4.3223 1.13194C4.37088 1.08363 4.42856 1.04533 4.49204 1.01921C4.55551 0.993097 4.62355 0.979686 4.69225 0.979748Z"
fill="currentColor"
/>
<path
id="Vector_2"
d="M6.38171 11.9358C9.42806 11.9358 9.23786 10.622 9.23786 10.622L9.23447 9.26098H6.32735V8.85235H10.3891C10.3891 8.85235 12.3384 9.07221 12.3384 6.01544C12.3384 2.95872 10.637 3.06713 10.637 3.06713H9.62156V4.48554C9.62156 4.48554 9.6763 6.17754 7.94727 6.17754H5.06397C5.06397 6.17754 3.44404 6.15151 3.44404 7.73449V10.3518C3.44404 10.3518 3.19806 11.9358 6.38166 11.9358H6.38171ZM7.98467 11.0206C7.91598 11.0206 7.84794 11.0072 7.78447 10.9811C7.72099 10.955 7.66331 10.9167 7.61473 10.8684C7.56616 10.8201 7.52764 10.7627 7.50138 10.6996C7.47512 10.6364 7.46163 10.5688 7.46169 10.5005C7.46163 10.4322 7.47511 10.3645 7.50136 10.3014C7.52762 10.2382 7.56614 10.1809 7.61472 10.1326C7.66329 10.0842 7.72097 10.0459 7.78445 10.0198C7.84794 9.9937 7.91597 9.98029 7.98467 9.98035C8.05337 9.98029 8.1214 9.9937 8.18488 10.0198C8.24836 10.0459 8.30604 10.0842 8.35462 10.1325C8.40319 10.1808 8.44171 10.2382 8.46797 10.3013C8.49423 10.3645 8.50772 10.4321 8.50766 10.5004C8.50772 10.5687 8.49423 10.6364 8.46797 10.6995C8.44171 10.7627 8.40319 10.82 8.35462 10.8683C8.30604 10.9166 8.24836 10.9549 8.18488 10.981C8.1214 11.0072 8.05337 11.0206 7.98467 11.0205V11.0206Z"
fill="currentColor"
/>
</g>
</svg>
<Link to={`/extensions/detail?id=${server.id}`} className="home-page-server-name">
{server.name}
</Link>
</div>
</div>
<div className="card-content">
<div>
<div>
<p className="card-description">{server.description}</p>
</div>
<div className="py-4">
{server.is_builtin ? (
<div className="flex items-center gap-2">
<Info
style={{ width: "12px", height: "12px" }}
className="text-textSubtle shrink-0"
/>
<span
style={{ fontSize: "12px" }}
className="text-textSubtle leading-normal"
>
Can be enabled in the goose settings page
</span>
</div>
) : (
<>
<button
onClick={() => setIsCommandVisible(!isCommandVisible)}
className="command-toggle"
>
<Terminal className="h-4 w-4" />
<h4 className="mx-2">Command</h4>
<ChevronRight
className={`ml-auto transition-transform ${
isCommandVisible ? "rotate-90" : ""
}`}
/>
</button>
<AnimatePresence>
{isCommandVisible && (
<motion.div
className="command-content"
initial={{ opacity: 0, translateY: -20 }}
animate={{ opacity: 1, translateY: 0 }}
exit={{
opacity: 0,
translateY: -20,
transition: { duration: 0.1 },
}}
>
<code>
{`goose session --with-extension "${server.command}"`}
</code>
</motion.div>
)}
</AnimatePresence>
</>
)}
</div>
</div>
<div className="card-footer">
<Link
to={server.link}
className="card-stats"
onClick={(e) => e.stopPropagation()}
>
<Star className="h-4 w-4" />
<span>{server.githubStars} on Github</span>
</Link>
<div className="card-action">
{server.is_builtin ? (
<div
className="built-in-badge"
title="This extension is built into goose and can be enabled in the settings page"
>
Built-in
</div>
) : (
<a
href={getGooseInstallLink(server)}
target="_blank"
rel="noopener noreferrer"
className="install-button"
>
<span>Install</span>
<Download className="h-4 w-4" />
</a>
)}
</div>
</div>
</div>
</div>
</div>
</div>
);
}
+26
View File
@@ -0,0 +1,26 @@
import React from "react";
interface BadgeProps {
children: React.ReactNode;
className?: string;
variant?: "default" | "secondary";
}
export const Badge: React.FC<BadgeProps> = ({
children,
className = "",
variant = "default",
}) => {
const baseStyles =
"inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium";
const variantStyles = {
default: "bg-purple-100 text-purple-800",
secondary: "bg-gray-100 text-gray-800",
};
return (
<span className={`${baseStyles} ${variantStyles[variant]} ${className}`}>
{children}
</span>
);
};
@@ -0,0 +1,37 @@
import React from "react";
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: "default" | "ghost" | "link";
size?: "default" | "icon";
}
export const Button: React.FC<ButtonProps> = ({
children,
className = "",
variant = "default",
size = "default",
...props
}) => {
const baseStyles =
"flex rounded-full focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-accent dark:focus:ring-offset-gray-900";
const variantStyles = {
default:
"bg-black dark:bg-white text-white dark:text-black hover:bg-accent/90 dark:hover:bg-accent/80",
ghost:
"bg-transparent hover:bg-gray-100 dark:hover:bg-gray-700 dark:text-gray-300",
link: "bg-transparent text-accent hover:underline hover:text-textProminent dark:text-accent/90",
};
const sizeStyles = {
default: "px-6 py-3",
icon: "p-2",
};
return (
<button
className={`${baseStyles} ${variantStyles[variant]} ${sizeStyles[size]} ${className}`}
{...props}
>
{children}
</button>
);
};
+41
View File
@@ -0,0 +1,41 @@
import React from "react";
export const Card: React.FC<React.HTMLAttributes<HTMLDivElement>> = ({
children,
className = "",
...props
}) => {
return (
<div
className={`bg-bgApp p-4 rounded-2xl border border-borderSubtle
${className}`}
{...props}
>
{children}
</div>
);
};
export const CardHeader: React.FC<React.HTMLAttributes<HTMLDivElement>> = ({
children,
className = "",
...props
}) => {
return (
<div className={`text-[18px] leading-[24px] mb-2 ${className}`} {...props}>
{children}
</div>
);
};
export const CardContent: React.FC<React.HTMLAttributes<HTMLDivElement>> = ({
children,
className = "",
...props
}) => {
return (
<div className={`${className}`} {...props}>
{children}
</div>
);
};
+12
View File
@@ -0,0 +1,12 @@
import React from "react";
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {}
export const Input: React.FC<InputProps> = ({ className = "", ...props }) => {
return (
<input
className={`bg-bgApp font-light text-textProminent placeholder-textPlaceholder w-full px-3 py-3 text-[40px] leading-[52px] border-b border-borderSubtle focus:outline-none focus:ring-purple-500 focus:border-borderProminent caret-[#FF4F00] ${className}`}
{...props}
/>
);
};
+74 -5
View File
@@ -49,6 +49,25 @@
--ifm-code-font-size: 95%;
--docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.1);
/* Additional Arcade color system variables */
--background-app: var(--constant-white);
--background-prominent: var(--grey-80);
--background-standard: var(--grey-90);
--background-subtle: var(--grey-95);
--border-divider: var(--grey-90);
--border-inverse: var(--constant-white);
--border-prominent: var(--grey-10);
--border-standard: var(--grey-60);
--border-subtle: var(--grey-90);
--icon-disabled: var(--grey-60);
--icon-extra-subtle: var(--grey-60);
--icon-inverse: var(--constant-white);
--icon-prominent: var(--grey-10);
--icon-standard: var(--grey-20);
--icon-subtle: var(--grey-50);
/* arcade colors */
--constant-white: #ffffff;
--constant-black: #000000;
@@ -162,8 +181,7 @@
/* video adnomition */
--ifm-color-video-alert-contrast-background: #336e62;
--ifm-color-video-alert-contrast-foreground: rgb(216 251 216);
--ifm-color-video-alert-border: #99d5c5
;
--ifm-color-video-alert-border: #99d5c5;
}
/* overrides */
@@ -255,13 +273,64 @@ html[data-theme="light"] .hide-in-light {
}
.alert--video {
--ifm-alert-background-color: var(--ifm-color-video-alert-contrast-background);
--ifm-alert-background-color: var(
--ifm-color-video-alert-contrast-background
);
--ifm-alert-background-color-highlight: rgba(84, 199, 236, 0.15);
--ifm-alert-foreground-color: var(--ifm-color-video-alert-contrast-foreground);
--ifm-alert-foreground-color: var(
--ifm-color-video-alert-contrast-foreground
);
--ifm-alert-border-color: var(--ifm-color-video-alert-border);
}
.aspect-ratio{
aspect-ratio: 16 / 9;
width: 100%;
}
}
.navbar {
border-bottom: 1px solid var(--border-divider);
}
.navbar__item {
display: flex;
align-items: center;
}
.iconExternalLink_nPIU {
margin-left: 8px !important;
}
.mx-auto {
margin-left: auto;
margin-right: auto;
}
@media (min-width: 1536px) {
.container {
max-width: 1536px;
}
}
@media (min-width: 1280px) {
.container {
max-width: 1280px;
}
}
@media (min-width: 1024px) {
.container {
max-width: 1024px;
}
}
@media (min-width: 768px) {
.container {
max-width: 768px;
}
}
@media (min-width: 640px) {
.container {
max-width: 640px;
}
}
.container {
width: 100%;
}
+296
View File
@@ -0,0 +1,296 @@
/* Animation for card hover effect */
@keyframes rotate {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
/* Search styling */
.search-container {
position: relative;
margin-bottom: 2rem;
}
.search-input {
width: 100%;
padding: 1rem 0;
font-size: 1rem;
color: var(--text-standard);
background: transparent;
border: none;
border-bottom: 1px solid var(--border-subtle);
transition: border-color 0.2s;
}
.search-input:focus {
outline: none;
border-bottom-color: var(--border-standard);
}
.search-input::placeholder {
color: var(--text-placeholder);
}
/* Card base styles */
.server-card {
position: relative;
height: 100%;
padding: 2px;
overflow: hidden;
border-radius: 17px;
background-color: var(--border-subtle);
transition: all 0.3s ease;
}
.server-card.interactive:hover {
background-color: transparent;
transform: translateY(-2px);
}
.server-card:hover .card-glow {
opacity: 1;
}
.card-glow {
position: absolute;
opacity: 0;
pointer-events: none;
width: 600px;
height: 600px;
top: -150px;
left: -50px;
transform-origin: center;
background: linear-gradient(45deg, #13bbaf, #ff4f00);
animation: rotate 6s linear infinite;
z-index: -1;
transition: opacity 0.2s;
}
.card {
height: 100%;
display: flex;
flex-direction: column;
background-color: var(--background-app);
border-radius: 15px;
box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
border: 1px solid var(--border-subtle);
overflow: hidden;
transition: all 0.3s ease;
}
/* Card header styles */
.card-header {
padding: 1.5rem 1.5rem 0.75rem 1.5rem;
display: flex;
align-items: center;
}
.card-header-content {
display: flex;
align-items: center;
gap: 0.5rem;
width: 100%;
}
/* Extension name and icon layout */
.extension-title {
display: flex;
align-items: center;
gap: 0.5rem;
text-decoration: none !important;
color: var(--text-standard);
transition: color 0.2s;
}
.extension-title:hover {
color: var(--text-prominent);
text-decoration: none !important;
}
.extension-icon {
flex-shrink: 0;
width: 13px;
height: 12px;
}
.home-page-server-name {
font-size: 1rem;
font-weight: 500;
line-height: 1.2;
}
.home-page-server-name:hover {
color: var(--text-prominent);
text-decoration: none;
}
/* Card content layout */
.card-content {
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
padding: 0 1.5rem 1.5rem 1.5rem;
gap: 1rem;
}
.card-description {
font-size: 0.875rem;
line-height: 1.5;
color: var(--text-standard);
margin: 0;
}
/* Command section styles */
.command-toggle {
display: flex;
align-items: center;
width: 100%;
padding: 0.5rem 0;
font-size: 0.875rem;
color: var(--text-standard);
background: transparent;
border: none;
cursor: pointer;
transition: all 0.2s ease;
}
.command-toggle:hover {
color: var(--text-prominent);
}
.command-toggle h4 {
font-size: 0.875rem;
font-weight: 500;
margin: 0;
}
.command-toggle svg {
width: 16px;
height: 16px;
}
.command-content {
background-color: var(--background-subtle);
padding: 0.75rem;
border-radius: 0.375rem;
font-size: 0.875rem;
color: var(--text-standard);
margin-top: 0.5rem;
}
.command-content code {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
"Liberation Mono", "Courier New", monospace;
font-size: 0.875rem;
background-color: transparent;
border: none;
}
/* Card footer */
.card-footer {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: auto;
}
/* GitHub stats link */
.card-stats {
display: flex;
align-items: center;
gap: 0.25rem;
font-size: 0.75rem;
color: var(--text-subtle);
transition: color 0.2s;
text-decoration: none !important;
}
.card-stats:hover {
color: var(--text-prominent);
text-decoration: none !important;
}
.card-stats svg {
width: 16px;
height: 16px;
}
/* Card actions (Install button/Built-in badge) */
.card-action {
margin-left: auto;
}
.install-button {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.75rem;
color: var(--text-subtle);
transition: all 0.2s ease;
text-decoration: none !important;
padding: 0;
}
.install-button:hover {
color: var(--text-prominent);
text-decoration: none !important;
}
.install-button svg {
width: 16px;
height: 16px;
transition: color 0.2s;
}
.install-button:hover svg {
color: #fa5204;
}
.built-in-badge {
font-size: 0.75rem;
padding: 0.25rem 0.5rem;
border-radius: 999px;
background-color: var(--background-subtle);
color: var(--text-subtle);
cursor: help;
}
/* Grid layout */
.cards-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
gap: 1.5rem;
width: 100%;
padding: 1rem 0;
}
@media (min-width: 768px) {
.cards-grid {
grid-template-columns: repeat(2, 1fr);
}
}
@media (min-width: 1024px) {
.cards-grid {
grid-template-columns: repeat(3, 1fr);
}
}
/* Dark mode adjustments */
html[data-theme="dark"] .card {
background-color: var(--background-app);
border-color: var(--border-subtle);
box-shadow: none;
}
html[data-theme="dark"] .command-content {
background-color: rgba(255, 255, 255, 0.05);
}
html[data-theme="dark"] .built-in-badge {
background-color: rgba(255, 255, 255, 0.1);
}
+75
View File
@@ -0,0 +1,75 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
/* start arcade colors */
--constant-white: #ffffff;
--constant-black: #000000;
--grey-10: #101010;
--grey-20: #1e1e1e;
--grey-50: #666666;
--grey-60: #959595;
--grey-80: #cccccc;
--grey-85: #dadada;
--grey-90: #e8e8e8;
--grey-95: #f0f0f0;
--dark-grey-15: #1a1a1a;
--dark-grey-25: #232323;
--dark-grey-30: #2a2a2a;
--dark-grey-40: #333333;
--dark-grey-45: #595959;
--dark-grey-60: #878787;
--dark-grey-90: #e1e1e1;
--background-app: var(--constant-white);
--background-prominent: var(--grey-80);
--background-standard: var(--grey-90);
--background-subtle: var(--grey-95);
--border-divider: var(--grey-90);
--border-inverse: var(--constant-white);
--border-prominent: var(--grey-10);
--border-standard: var(--grey-60);
--border-subtle: var(--grey-90);
--icon-disabled: var(--grey-60);
--icon-extra-subtle: var(--grey-60);
--icon-inverse: var(--constant-white);
--icon-prominent: var(--grey-10);
--icon-standard: var(--grey-20);
--icon-subtle: var(--grey-50);
--text-placeholder: var(--grey-60);
--text-prominent: var(--grey-10);
--text-standard: var(--grey-20);
--text-subtle: var(--grey-50);
&.dark {
--background-app: var(--constant-black);
--background-prominent: var(--dark-grey-40);
--background-standard: var(--dark-grey-25);
--background-subtle: var(--dark-grey-15);
--border-divider: var(--dark-grey-25);
--border-inverse: var(--constant-black);
--border-prominent: var(--constant-white);
--border-standard: var(--dark-grey-45);
--border-subtle: var(--dark-grey-25);
--icon-disabled: var(--dark-grey-45);
--icon-extra-subtle: var(--dark-grey-45);
--icon-inverse: var(--constant-black);
--icon-prominent: var(--constant-white);
--icon-standard: var(--dark-grey-90);
--icon-subtle: var(--dark-grey-60);
--text-placeholder: var(--dark-grey-45);
--text-prominent: var(--constant-white);
--text-standard: var(--dark-grey-90);
--text-subtle: var(--dark-grey-60);
}
/* end arcade colors */
}
}
@@ -0,0 +1,259 @@
import Layout from "@theme/Layout";
import { Download, Terminal, Star, ArrowLeft, Info } from "lucide-react";
import { Button } from "@site/src/components/ui/button";
import { Badge } from "@site/src/components/ui/badge";
import { getGooseInstallLink } from "@site/src/utils/install-links";
import { useLocation } from "@docusaurus/router";
import { useEffect, useState } from "react";
import type { MCPServer } from "@site/src/types/server";
import { fetchMCPServers } from "@site/src/utils/mcp-servers";
import Link from "@docusaurus/Link";
function ExtensionDetail({ server }: { server: MCPServer }) {
return (
<Layout>
<div className="min-h-screen flex items-start justify-center py-16">
<div className="container max-w-5xl mx-auto px-4">
<div className="flex gap-8">
<div>
<Link to="/extensions" className="no-underline">
<Button
variant="ghost"
className="flex items-center gap-2 text-textSubtle hover:text-textProminent"
>
<ArrowLeft className="h-4 w-4" />
Back
</Button>
</Link>
</div>
<div className="server-card flex-1">
<div className="card p-8">
<div className="card-header mb-6">
<div className="flex items-center gap-4">
<h1 className="font-medium text-5xl text-textProminent m-0">
{server.name}
</h1>
{server.is_builtin && (
<Badge variant="secondary" className="text-sm">
Built-in
</Badge>
)}
</div>
</div>
<div className="card-content space-y-6">
<div>
<p className="text-xl text-textSubtle m-0">
{server.description}
</p>
</div>
{server.installation_notes && (
<div>
<p className="text-md text-textSubtle m-0">
{server.installation_notes}
</p>
</div>
)}
<div className="space-y-2">
{server.is_builtin ? (
<div className="flex items-center gap-2">
<Info className="h-4 w-4 text-textSubtle shrink-0" />
<span className="text-sm text-textSubtle">
Can be enabled in the goose settings page
</span>
</div>
) : (
<>
<div className="flex items-center gap-2 text-textStandard">
<Terminal className="h-4 w-4" />
<h4 className="font-medium m-0">Command</h4>
</div>
<div className="command-content">
<code className="text-sm block">
{`goose session --with-extension "${server.command}"`}
</code>
</div>
</>
)}
</div>
{server.environmentVariables && (
<div className="space-y-4">
<h2 className="text-lg font-medium text-textStandard m-0">
Environment Variables
</h2>
{server.environmentVariables.length > 0 ? (
<div>
{server.environmentVariables.map((env) => (
<div
key={env.name}
className="border-b border-borderSubtle py-4 first:pt-0 last:border-0"
>
<div className="text-sm text-textStandard font-medium">
{env.name}
</div>
<div className="text-textSubtle text-sm mt-1">
{env.description}
</div>
{env.required && (
<Badge variant="secondary" className="mt-2">
Required
</Badge>
)}
</div>
))}
</div>
) : (
<div className="text-textSubtle text-sm flex items-center gap-2">
<Info className="h-4 w-4" />
No environment variables needed
</div>
)}
</div>
)}
<div className="card-footer">
<a
href={server.link}
target="_blank"
rel="noopener noreferrer"
className="card-stats"
>
<Star className="h-4 w-4" />
<span>{server.githubStars} on Github</span>
</a>
{server.is_builtin ? (
<div
className="built-in-badge"
title="This extension is built into goose and can be enabled in the settings page"
>
Built-in
</div>
) : (
<a
href={getGooseInstallLink(server)}
target="_blank"
rel="noopener noreferrer"
className="install-button"
>
<span>Install</span>
<Download className="h-4 w-4" />
</a>
)}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</Layout>
);
}
export default function DetailPage(): JSX.Element {
const location = useLocation();
const [server, setServer] = useState<MCPServer | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const loadServer = async () => {
try {
setLoading(true);
setError(null);
const servers = await fetchMCPServers();
// Get the ID from the query parameter
const params = new URLSearchParams(location.search);
const id = params.get("id");
if (!id) {
setError("No extension ID provided");
return;
}
const foundServer = servers.find((s) => s.id === id);
if (foundServer) {
setServer(foundServer);
} else {
setError("Extension not found");
}
} catch (err) {
setError("Failed to load extension details");
console.error(err);
} finally {
setLoading(false);
}
};
loadServer();
}, [location]);
if (loading) {
return (
<Layout>
<div className="min-h-screen flex items-start justify-center py-16">
<div className="container max-w-5xl mx-auto px-4">
<div className="flex gap-8">
<div>
<Link to="/extensions" className="no-underline">
<Button
variant="ghost"
className="flex items-center gap-2 text-textSubtle hover:text-textProminent"
>
<ArrowLeft className="h-4 w-4" />
Back
</Button>
</Link>
</div>
<div className="server-card flex-1">
<div className="card p-8">
<div className="animate-pulse">
<div className="h-12 w-48 bg-bgSubtle rounded-lg mb-4"></div>
<div className="h-6 w-full bg-bgSubtle rounded-lg mb-2"></div>
<div className="h-6 w-2/3 bg-bgSubtle rounded-lg"></div>
</div>
</div>
</div>
</div>
</div>
</div>
</Layout>
);
}
if (error || !server) {
return (
<Layout>
<div className="min-h-screen flex items-start justify-center py-16">
<div className="container max-w-5xl mx-auto px-4">
<div className="flex gap-8">
<div>
<Link to="/extensions" className="no-underline">
<Button
variant="ghost"
className="flex items-center gap-2 text-textSubtle hover:text-textProminent"
>
<ArrowLeft className="h-4 w-4" />
Back
</Button>
</Link>
</div>
<div className="server-card flex-1">
<div className="card p-8">
<div className="text-red-500">
{error || "Extension not found"}
</div>
</div>
</div>
</div>
</div>
</div>
</Layout>
);
}
return <ExtensionDetail server={server} />;
}
@@ -0,0 +1,118 @@
import { Search } from "lucide-react";
import { Button } from "@site/src/components/ui/button";
import { Input } from "@site/src/components/ui/input";
import { ServerCard } from "@site/src/components/server-card";
import { useState, useEffect } from "react";
import type { MCPServer } from "@site/src/types/server";
import { fetchMCPServers, searchMCPServers } from "@site/src/utils/mcp-servers";
import { motion } from "framer-motion";
import Layout from "@theme/Layout";
export default function HomePage() {
const [servers, setServers] = useState<MCPServer[]>([]);
const [searchQuery, setSearchQuery] = useState("");
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
// Combined effect for initial load and search
useEffect(() => {
const loadServers = async () => {
try {
setIsLoading(true);
setError(null);
const trimmedQuery = searchQuery.trim();
const results = trimmedQuery
? await searchMCPServers(trimmedQuery)
: await fetchMCPServers();
console.log("Loaded servers:", results);
setServers(results);
} catch (err) {
const errorMessage =
err instanceof Error ? err.message : "Unknown error";
setError(`Failed to load servers: ${errorMessage}`);
console.error("Error loading servers:", err);
} finally {
setIsLoading(false);
}
};
// Debounce all server loads
const timeoutId = setTimeout(loadServers, 300);
return () => clearTimeout(timeoutId);
}, [searchQuery]);
return (
<Layout>
<div className="container mx-auto px-4 p-24">
<div className="pb-16">
<h1 className="text-[64px] font-medium text-textProminent">
Browse Extensions
</h1>
<p className="text-textProminent">
Your central directory for discovering and installing extensions.
</p>
</div>
<div className="search-container">
<input
className="bg-bgApp font-light text-textProminent placeholder-textPlaceholder w-full px-3 py-3 text-[40px] leading-[52px] border-b border-borderSubtle focus:outline-none focus:ring-purple-500 focus:border-borderProminent caret-[#FF4F00] pl-0"
placeholder="Search for extensions"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
</div>
{error && (
<div className="p-4 bg-red-50 text-red-600 rounded-md">{error}</div>
)}
<section className="">
<div className={`${searchQuery ? "pb-2" : "pb-8"}`}>
<p className="text-gray-600">
{searchQuery
? `${servers.length} result${
servers.length > 1 ? "s" : ""
} for "${searchQuery}"`
: ""}
</p>
</div>
{isLoading ? (
<div className="py-8 text-xl text-gray-600">Loading servers...</div>
) : servers.length === 0 ? (
<div className="text-center py-8 text-gray-500">
{searchQuery
? "No servers found matching your search."
: "No servers available."}
</div>
) : (
<div className="cards-grid">
{servers
.sort((a, b) => {
// Sort built-in servers first
if (a.is_builtin && !b.is_builtin) return -1;
if (!a.is_builtin && b.is_builtin) return 1;
return 0;
})
.map((server) => (
<motion.div
key={server.id}
initial={{
opacity: 0,
}}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.6 }}
>
<ServerCard key={server.id} server={server} />
</motion.div>
))}
</div>
)}
</section>
</div>
</Layout>
);
}
@@ -0,0 +1,133 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@font-face {
font-family: Cash Sans;
src: url(https://cash-f.squarecdn.com/static/fonts/cashsans/woff2/CashSans-Regular.woff2)
format("woff2");
font-weight: 400;
font-style: normal;
}
@font-face {
font-family: Cash Sans;
src: url(https://cash-f.squarecdn.com/static/fonts/cashsans/woff2/CashSans-Medium.woff2)
format("woff2");
font-weight: 500;
font-style: normal;
}
@font-face {
font-family: Cash Sans;
src: url(https://cash-f.squarecdn.com/static/fonts/cashsans/woff2/CashSans-Semibold.woff2)
format("woff2");
font-weight: 600;
font-style: normal;
}
@font-face {
font-family: Cash Sans;
src: url(https://cash-f.squarecdn.com/static/fonts/cashsans/woff2/CashSans-Bold.woff2)
format("woff2");
font-weight: 700;
font-style: normal;
}
@layer base {
:root {
/* start arcade colors */
--constant-white: #ffffff;
--constant-black: #000000;
--grey-10: #101010;
--grey-20: #1e1e1e;
--grey-50: #666666;
--grey-60: #959595;
--grey-80: #cccccc;
--grey-85: #dadada;
--grey-90: #e8e8e8;
--grey-95: #f0f0f0;
--dark-grey-15: #1a1a1a;
--dark-grey-25: #232323;
--dark-grey-30: #2a2a2a;
--dark-grey-40: #333333;
--dark-grey-45: #595959;
--dark-grey-60: #878787;
--dark-grey-90: #e1e1e1;
--background-app: var(--constant-white);
--background-prominent: var(--grey-80);
--background-standard: var(--grey-90);
--background-subtle: var(--grey-95);
--border-divider: var(--grey-90);
--border-inverse: var(--constant-white);
--border-prominent: var(--grey-10);
--border-standard: var(--grey-60);
--border-subtle: var(--grey-90);
--icon-disabled: var(--grey-60);
--icon-extra-subtle: var(--grey-60);
--icon-inverse: var(--constant-white);
--icon-prominent: var(--grey-10);
--icon-standard: var(--grey-20);
--icon-subtle: var(--grey-50);
--text-placeholder: var(--grey-60);
--text-prominent: var(--grey-10);
--text-standard: var(--grey-20);
--text-subtle: var(--grey-50);
&.dark {
--background-app: var(--constant-black);
--background-prominent: var(--dark-grey-40);
--background-standard: var(--dark-grey-25);
--background-subtle: var(--dark-grey-15);
--border-divider: var(--dark-grey-25);
--border-inverse: var(--constant-black);
--border-prominent: var(--constant-white);
--border-standard: var(--dark-grey-45);
--border-subtle: var(--dark-grey-25);
--icon-disabled: var(--dark-grey-45);
--icon-extra-subtle: var(--dark-grey-45);
--icon-inverse: var(--constant-black);
--icon-prominent: var(--constant-white);
--icon-standard: var(--dark-grey-90);
--icon-subtle: var(--dark-grey-60);
--text-placeholder: var(--dark-grey-45);
--text-prominent: var(--constant-white);
--text-standard: var(--dark-grey-90);
--text-subtle: var(--dark-grey-60);
}
/* end arcade colors */
}
}
@keyframes rotate {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
@keyframes appear-top {
0% {
transform: translateY(-50%);
opacity: 0;
}
100% {
transform: translateY(0);
opacity: 1;
}
}
/* view transitions */
a.transitioning .home-page-server-name,
.detail-page-server-name {
view-transition-name: server-name;
}
+16
View File
@@ -0,0 +1,16 @@
export interface MCPServer {
id: string;
name: string;
description: string;
command: string;
link: string;
installation_notes: string;
is_builtin: boolean;
endorsed: boolean
githubStars: number;
environmentVariables: {
name: string;
description: string;
required: boolean;
}[];
}
+5
View File
@@ -0,0 +1,5 @@
import type { MCPServer } from "../types/server";
export function getGooseInstallLink(server: MCPServer): string {
return `goose://install/${encodeURIComponent(server.command)}`;
}
+30
View File
@@ -0,0 +1,30 @@
import type { MCPServer } from "../types/server";
export function getGooseInstallLink(server: MCPServer): string {
if (server.is_builtin) {
const queryParams = [
'cmd=goosed',
'arg=mcp',
`arg=${encodeURIComponent(server.id)}`,
`description=${encodeURIComponent(server.id)}`
].join('&');
return `goose://extension?${queryParams}`;
}
const parts = server.command.split(" ");
const baseCmd = parts[0]; // npx or uvx
const args = parts.slice(1); // remaining arguments
const queryParams = [
`cmd=${encodeURIComponent(baseCmd)}`,
...args.map((arg) => `arg=${encodeURIComponent(arg)}`),
`id=${encodeURIComponent(server.id)}`,
`name=${encodeURIComponent(server.name)}`,
`description=${encodeURIComponent(server.description)}`,
...server.environmentVariables
.filter((env) => env.required)
.map(
(env) => `env=${encodeURIComponent(`${env.name}=${env.description}`)}`
),
].join("&");
return `goose://extension?${queryParams}`;
}
+33
View File
@@ -0,0 +1,33 @@
import type { MCPServer } from "../types/server";
const SERVERS_URL = "/goose/servers.json";
export async function fetchMCPServers(): Promise<MCPServer[]> {
try {
const response = await fetch(SERVERS_URL);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
console.log('Fetched MCP servers data:', data);
return data;
} catch (error) {
console.error("Error fetching MCP servers:", error);
throw error;
}
}
export async function searchMCPServers(query: string): Promise<MCPServer[]> {
const servers = await fetchMCPServers();
const normalizedQuery = query.toLowerCase();
return servers.filter((server) => {
const normalizedName = server.name.toLowerCase();
const normalizedDescription = server.description.toLowerCase();
return (
normalizedName.includes(normalizedQuery) ||
normalizedDescription.includes(normalizedQuery)
);
});
}