Docs: Revamp extensions site (#1260)
Co-authored-by: Nahiyan Khan <nahiyan@squareup.com>
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -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%;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}[];
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import type { MCPServer } from "../types/server";
|
||||
|
||||
export function getGooseInstallLink(server: MCPServer): string {
|
||||
return `goose://install/${encodeURIComponent(server.command)}`;
|
||||
}
|
||||
@@ -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}`;
|
||||
}
|
||||
@@ -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)
|
||||
);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user