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
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import YouTubeShortEmbed from '@site/src/components/YouTubeShortEmbed';

<YouTubeShortEmbed videoUrl="https://www.youtube.com/embed/4diEvoRFVrQ" />

This tutorial covers how to add the [Cloudinary Asset Management MCP Server](https://github.com/cloudinary-community/cloudinary-mcp) as a Goose extension to automate complex image processing workflows that would typically require specialized design software or manual editing.
This tutorial covers how to add the [Cloudinary Asset Management MCP Server](https://github.com/cloudinary/asset-management-js) as a Goose extension to automate complex image processing workflows that would typically require specialized design software or manual editing.

:::tip TLDR

Expand Down
1 change: 0 additions & 1 deletion documentation/src/components/prompt-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ function extensionToMCPServer(extension: Extension): MCPServer {
installation_notes: extension.installation_notes || '',
endorsed: false,
environmentVariables: extension.environmentVariables || [],
githubStars: 0
};
}

Expand Down
71 changes: 27 additions & 44 deletions documentation/src/components/server-card.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,34 @@
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 { useState, useEffect } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { getGooseInstallLink } from "@site/src/utils/install-links";
import { fetchGitHubStars, formatStarCount } from "@site/src/utils/github-stars";

const getExtensionCommand = (server: MCPServer): string => {
switch (server.type) {
case "remote":
return `goose session --with-remote-extension "${server.url}"`;
case "streamable-http":
return `goose session --with-streamable-http-extension "${server.url}"`;
case "local":
default:
return `goose session --with-extension "${server.command}"`;
}
};

export function ServerCard({ server }: { server: MCPServer }) {
const [isCommandVisible, setIsCommandVisible] = useState(false);
const [githubStars, setGithubStars] = useState<number | null>(null);

useEffect(() => {
if (server.link) {
fetchGitHubStars(server.link).then(stars => {
setGithubStars(stars);
});
}
}, [server.link]);

return (
<div className="extension-title h-full">
Expand Down Expand Up @@ -65,42 +85,7 @@ export function ServerCard({ server }: { server: MCPServer }) {
</div>
)}

{(!server.is_builtin && server.command !== undefined && server.url === undefined) && (
<>
<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>
</>
)}

{(!server.is_builtin && server.command === undefined && server.url !== undefined) && (
{!server.is_builtin && (
<>
<button
onClick={() => setIsCommandVisible(!isCommandVisible)}
Expand All @@ -126,9 +111,7 @@ export function ServerCard({ server }: { server: MCPServer }) {
transition: { duration: 0.1 },
}}
>
<code>
{`goose session --with-remote-extension "${server.url}"`}
</code>
<code>{getExtensionCommand(server)}</code>
</motion.div>
)}
</AnimatePresence>
Expand All @@ -138,14 +121,14 @@ export function ServerCard({ server }: { server: MCPServer }) {
</div>

<div className="card-footer">
{server.githubStars !== undefined && (
{githubStars !== null && (
<Link
to={server.link}
className="card-stats"
onClick={(e) => e.stopPropagation()}
>
<Star className="h-4 w-4" />
<span>{server.githubStars} on Github</span>
<span>{formatStarCount(githubStars)} on Github</span>
</Link>
)}
<div className="card-action">
Expand Down
84 changes: 69 additions & 15 deletions documentation/src/pages/extensions/detail.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,46 @@
import Layout from "@theme/Layout";
import { Download, Terminal, Star, ArrowLeft, Info } from "lucide-react";
import { Download, Terminal, Star, ArrowLeft, Info, BookOpen } 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";
import { fetchGitHubStars, formatStarCount } from "@site/src/utils/github-stars";
import Link from "@docusaurus/Link";

function ExtensionDetail({ server }: { server: MCPServer }) {
const [githubStars, setGithubStars] = useState<number | null>(null);


// outliers in naming
const overrides: Record<string, string> = {
'computercontroller': 'computer-controller-mcp',
'pdf-read': 'pdf-mcp',
'knowledge-graph-memory': 'knowledge-graph-mcp'
};

const getDocumentationPath = (serverId: string): string => {
let filename = serverId.replace(/_/g, '-');
filename = overrides[filename] ?? filename;

if (!filename.endsWith('-mcp')) {
filename += '-mcp';
}

return filename;
};


useEffect(() => {
if (server.link) {
fetchGitHubStars(server.link).then(stars => {
setGithubStars(stars);
});
}
}, [server.link]);

return (
<Layout>
<div className="min-h-screen flex items-start justify-center py-16">
Expand All @@ -27,7 +58,14 @@ function ExtensionDetail({ server }: { server: MCPServer }) {
</div>

<div className="server-card flex-1">
<div className="card p-8">
<div className="card p-8 relative">
<Link
to={`/docs/mcp/${getDocumentationPath(server.id)}`}
className="absolute top-4 right-4 flex items-center gap-2 text-textSubtle hover:text-textProminent transition-colors no-underline"
title="View tutorial"
>
<BookOpen className="h-5 w-5" />
</Link>
<div className="card-header mb-6">
<div className="flex items-center gap-4">
<h1 className="font-medium text-5xl text-textProminent m-0">
Expand Down Expand Up @@ -71,9 +109,23 @@ function ExtensionDetail({ server }: { server: MCPServer }) {
<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>
{(server.type === "local" || !server.type) ? (
<code className="text-sm block">
{`goose session --with-extension "${server.command}"`}
</code>
) : server.type === "remote" ? (
<code className="text-sm block">
{`goose session --with-remote-extension "${server.url}"`}
</code>
) : server.type === "streamable-http" ? (
<code className="text-sm block">
{`goose session --with-streamable-http-extension "${server.url}"`}
</code>
) : (
<code className="text-sm block">
No command available
</code>
)}
</div>
</>
)}
Expand Down Expand Up @@ -115,15 +167,17 @@ function ExtensionDetail({ server }: { server: MCPServer }) {
)}

<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>
{githubStars !== null && (
<a
href={server.link}
target="_blank"
rel="noopener noreferrer"
className="card-stats"
>
<Star className="h-4 w-4" />
<span>{formatStarCount(githubStars)} on Github</span>
</a>
)}

{server.is_builtin ? (
<div
Expand Down
1 change: 0 additions & 1 deletion documentation/src/pages/prompt-library/detail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ function extensionToMCPServer(extension: Extension): MCPServer {
installation_notes: extension.installation_notes || "",
endorsed: false,
environmentVariables: extension.environmentVariables || [],
githubStars: 0,
};
}

Expand Down
8 changes: 4 additions & 4 deletions documentation/src/types/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@ export interface MCPServer {
id: string;
name: string;
description: string;
command: string;
url: string;
command?: string;
url?: string;
type?: "local" | "remote" | "streamable-http";
link: string;
installation_notes: string;
is_builtin: boolean;
endorsed: boolean
githubStars: number;
endorsed: boolean;
environmentVariables: {
name: string;
description: string;
Expand Down
110 changes: 110 additions & 0 deletions documentation/src/utils/github-stars.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
/**
* Utility for fetching GitHub repository star counts dynamically
*/

interface GitHubStarsCache {
stars: number;
timestamp: number;
}

const CACHE_DURATION = 24 * 60 * 60 * 1000; // 24 hours in milliseconds

/**
* Extract owner/repo from GitHub URL
*/
function extractRepoFromUrl(repoUrl: string): string | null {
if (!repoUrl) return null;

const match = repoUrl.match(/^https?:\/\/(www\.)?github\.com\/([^\/]+\/[^\/]+)/);
if (!match) return null;

// Clean up any trailing paths (tree/main, blob/main, etc.)
const repo = match[2].replace(/\/(tree|blob)\/.*$/, '').replace(/[#?].*$/, '');
return repo;
}

/**
* Get cached star count if valid
*/
function getCachedStars(repo: string): number | null {
try {
const cacheKey = `github-stars-${repo}`;
const cached = localStorage.getItem(cacheKey);

if (!cached) return null;

const { stars, timestamp }: GitHubStarsCache = JSON.parse(cached);

// Check if cache is still valid (24 hours)
if (Date.now() - timestamp < CACHE_DURATION) {
return stars;
}

// Cache expired, remove it
localStorage.removeItem(cacheKey);
return null;
} catch {
return null;
}
}

/**
* Cache star count
*/
function setCachedStars(repo: string, stars: number): void {
try {
const cacheKey = `github-stars-${repo}`;
const cacheData: GitHubStarsCache = {
stars,
timestamp: Date.now()
};
localStorage.setItem(cacheKey, JSON.stringify(cacheData));
} catch {
// Ignore localStorage errors (e.g., quota exceeded)
}
}

/**
* Fetch GitHub stars for a repository
* Returns null if the API call fails (for hiding the star display)
*/
export async function fetchGitHubStars(repoUrl: string): Promise<number | null> {
const repo = extractRepoFromUrl(repoUrl);
if (!repo) return null;

const cachedStars = getCachedStars(repo);
if (cachedStars !== null) {
return cachedStars;
}

try {
const response = await fetch(`https://api.github.com/repos/${repo}`, {
headers: {
'Accept': 'application/vnd.github.v3+json',
}
});

if (!response.ok) {
return null;
}

const data = await response.json();
const stars = data.stargazers_count || 0;

setCachedStars(repo, stars);

return stars;
} catch {
return null;
}
}

/**
* Format star count for display
*/
export function formatStarCount(stars: number): string {
if (stars >= 1000) {
return `${(stars / 1000).toFixed(1)}k`;
}
return stars.toString();
}
1 change: 0 additions & 1 deletion documentation/src/utils/mcp-servers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ export async function fetchMCPServers(): Promise<MCPServer[]> {
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);
Expand Down
Loading
Loading