added some logging + error handling

This commit is contained in:
pashpashpash 2025-02-13 18:22:33 -08:00
parent 7d67000d1f
commit e3cfb405fb
5 changed files with 209 additions and 22 deletions

View file

@ -14,7 +14,7 @@ import { selectImages } from "../../integrations/misc/process-images"
import { getTheme } from "../../integrations/theme/getTheme"
import WorkspaceTracker from "../../integrations/workspace/WorkspaceTracker"
import { McpHub } from "../../services/mcp/McpHub"
import { McpMarketplaceCatalog, McpMarketplaceItem } from "../../shared/mcp"
import { McpDownloadResponse, McpMarketplaceCatalog, McpMarketplaceItem, McpServer } from "../../shared/mcp"
import { FirebaseAuthManager, UserInfo } from "../../services/auth/FirebaseAuthManager"
import { ApiProvider, ModelInfo } from "../../shared/api"
import { findLast } from "../../shared/array"
@ -404,7 +404,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
items: (response.data || []).map((item: any) => ({
...item,
githubStars: item.githubStars ?? 0,
downloads: item.downloads ?? 0,
downloadCount: item.downloadCount ?? 0,
tags: item.tags ?? [],
})),
}
@ -438,30 +438,59 @@ export class ClineProvider implements vscode.WebviewViewProvider {
private async downloadMcp(mcpId: string) {
try {
const response = await axios.post(
// First check if we already have this MCP server installed
const servers = this.mcpHub?.getServers() || []
const isInstalled = servers.some((server: McpServer) => {
try {
const config = JSON.parse(server.config)
const serverConfig = config.mcpServers[server.name]
const githubUrl = serverConfig.args?.find((arg: string) => arg.includes("github.com"))
return githubUrl?.includes(mcpId)
} catch {
return false
}
})
if (isInstalled) {
throw new Error("This MCP server is already installed")
}
// Fetch server details from marketplace
const response = await axios.post<McpDownloadResponse>(
"https://api.cline.bot/v1/mcp/download",
{ mcpId },
{
mcpId,
},
{
headers: {
"Content-Type": "application/json",
},
headers: { "Content-Type": "application/json" },
timeout: 10000,
},
)
if (!response.data) {
throw new Error("Invalid response from MCP download API")
throw new Error("Invalid response from MCP marketplace API")
}
console.log("[downloadMcp] Response from download API", { response })
const mcpDetails = response.data
// Validate required fields
if (!mcpDetails.githubUrl) {
throw new Error("Missing GitHub URL in MCP download response")
}
if (!mcpDetails.readmeContent) {
throw new Error("Missing README content in MCP download response")
}
// Send details to webview
await this.postMessageToWebview({
type: "mcpDownloadDetails",
mcpDownloadDetails: mcpDetails,
})
// Create a new task for Cline to set up the MCP server
// Create task with context from README
const task = `Set up the MCP server from ${mcpDetails.githubUrl}. Here's some additional context from the README:\n\n${mcpDetails.readmeContent}`
// Initialize task and show chat view
await this.initClineWithTask(task)
await this.postMessageToWebview({
type: "action",
@ -469,7 +498,23 @@ export class ClineProvider implements vscode.WebviewViewProvider {
})
} catch (error) {
console.error("Failed to download MCP:", error)
const errorMessage = error instanceof Error ? error.message : "Failed to download MCP"
let errorMessage = "Failed to download MCP"
if (axios.isAxiosError(error)) {
if (error.code === "ECONNABORTED") {
errorMessage = "Request timed out. Please try again."
} else if (error.response?.status === 404) {
errorMessage = "MCP server not found in marketplace."
} else if (error.response?.status === 500) {
errorMessage = "Internal server error. Please try again later."
} else if (!error.response && error.request) {
errorMessage = "Network error. Please check your internet connection."
}
} else if (error instanceof Error) {
errorMessage = error.message
}
// Show error in both notification and marketplace UI
vscode.window.showErrorMessage(errorMessage)
await this.postMessageToWebview({
type: "mcpDownloadDetails",

View file

@ -5,7 +5,7 @@ import { AutoApprovalSettings } from "./AutoApprovalSettings"
import { BrowserSettings } from "./BrowserSettings"
import { ChatSettings } from "./ChatSettings"
import { HistoryItem } from "./HistoryItem"
import { McpServer, McpMarketplaceCatalog, McpMarketplaceItem } from "./mcp"
import { McpServer, McpMarketplaceCatalog, McpMarketplaceItem, McpDownloadResponse } from "./mcp"
// webview will hold state
export interface ExtensionMessage {
@ -50,7 +50,7 @@ export interface ExtensionMessage {
mcpServers?: McpServer[]
mcpMarketplaceCatalog?: McpMarketplaceCatalog
error?: string
mcpDownloadDetails?: McpMarketplaceItem
mcpDownloadDetails?: McpDownloadResponse
}
export type Platform = "aix" | "darwin" | "freebsd" | "linux" | "openbsd" | "sunos" | "win32" | "unknown"

View file

@ -81,7 +81,7 @@ export interface McpMarketplaceItem {
readmeContent?: string
isRecommended: boolean
githubStars: number
downloads: number
downloadCount: number
createdAt: string
updatedAt: string
lastGithubSync: string
@ -90,3 +90,13 @@ export interface McpMarketplaceItem {
export interface McpMarketplaceCatalog {
items: McpMarketplaceItem[]
}
export interface McpDownloadResponse {
mcpId: string
githubUrl: string
name: string
author: string
description: string
readmeContent: string
requiresApiKey: boolean
}

View file

@ -9,7 +9,17 @@ interface McpMarketplaceCardProps {
}
const McpMarketplaceCard = ({ item, installedServers }: McpMarketplaceCardProps) => {
const isInstalled = installedServers.some((server) => server.name === item.mcpId)
const isInstalled = installedServers.some((server) => {
try {
const config = JSON.parse(server.config)
const serverConfig = config.mcpServers[server.name]
// Extract GitHub URL from args if it's an npm package
const githubUrl = serverConfig.args?.find((arg: string) => arg.includes("github.com"))
return githubUrl?.includes(item.mcpId) || githubUrl?.includes(item.githubUrl)
} catch {
return false
}
})
const [isDownloading, setIsDownloading] = useState(false)
useEffect(() => {
@ -94,7 +104,7 @@ const McpMarketplaceCard = ({ item, installedServers }: McpMarketplaceCardProps)
</div>
<div style={{ display: "flex", alignItems: "center", gap: "4px" }}>
<span className="codicon codicon-cloud-download" />
{item.downloads?.toLocaleString() ?? 0}
{item.downloadCount?.toLocaleString() ?? 0}
</div>
{item.requiresApiKey && (
<div style={{ display: "flex", alignItems: "center", gap: "4px" }}>

View file

@ -1,16 +1,70 @@
import { useEffect, useState } from "react"
import { useEffect, useMemo, useState } from "react"
import { VSCodeButton, VSCodeProgressRing } from "@vscode/webview-ui-toolkit/react"
import { McpMarketplaceItem } from "../../../../../src/shared/mcp"
import { useExtensionState } from "../../../context/ExtensionStateContext"
import { vscode } from "../../../utils/vscode"
import McpMarketplaceCard from "./McpMarketplaceCard"
const searchInputStyles = {
width: "100%",
padding: "4px 8px 4px 28px",
background: "var(--vscode-input-background)",
border: "1px solid var(--vscode-input-border)",
color: "var(--vscode-input-foreground)",
borderRadius: "2px",
outline: "none",
transition: "border-color 0.1s ease-in-out, opacity 0.1s ease-in-out",
}
const selectStyles = {
padding: "4px 8px",
background: "var(--vscode-dropdown-background)",
border: "1px solid var(--vscode-dropdown-border)",
color: "var(--vscode-dropdown-foreground)",
borderRadius: "2px",
outline: "none",
transition: "border-color 0.1s ease-in-out, opacity 0.1s ease-in-out",
}
const McpMarketplaceView = () => {
const { mcpServers } = useExtensionState()
const [items, setItems] = useState<McpMarketplaceItem[]>([])
const [isLoading, setIsLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [isRefreshing, setIsRefreshing] = useState(false)
const [searchQuery, setSearchQuery] = useState("")
const [selectedCategory, setSelectedCategory] = useState<string | null>(null)
const [sortBy, setSortBy] = useState<"downloadCount" | "stars" | "name">("downloadCount")
const categories = useMemo(() => {
const uniqueCategories = new Set(items.map((item) => item.category))
return Array.from(uniqueCategories).sort()
}, [items])
const filteredItems = useMemo(() => {
return items
.filter((item) => {
const matchesSearch =
searchQuery === "" ||
item.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
item.description.toLowerCase().includes(searchQuery.toLowerCase()) ||
item.tags.some((tag) => tag.toLowerCase().includes(searchQuery.toLowerCase()))
const matchesCategory = !selectedCategory || item.category === selectedCategory
return matchesSearch && matchesCategory
})
.sort((a, b) => {
switch (sortBy) {
case "downloadCount":
return b.downloadCount - a.downloadCount
case "stars":
return b.githubStars - a.githubStars
case "name":
return a.name.localeCompare(b.name)
default:
return 0
}
})
}, [items, searchQuery, selectedCategory, sortBy])
useEffect(() => {
const handleMessage = (event: MessageEvent) => {
@ -89,13 +143,79 @@ const McpMarketplaceView = () => {
return (
<div style={{ display: "flex", flexDirection: "column", gap: "12px", padding: "0 20px" }}>
<div style={{ display: "flex", justifyContent: "flex-end", marginBottom: "8px" }}>
<div
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
marginBottom: "16px",
gap: "16px",
}}>
<div style={{ display: "flex", gap: "8px", alignItems: "center", flex: 1 }}>
<div style={{ position: "relative", flex: 1 }}>
<input
type="text"
placeholder="Search MCPs..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="mcp-search-input"
style={searchInputStyles}
/>
<span
className="codicon codicon-search"
style={{
position: "absolute",
left: "8px",
top: "50%",
transform: "translateY(-50%)",
color: "var(--vscode-input-placeholderForeground)",
}}
/>
</div>
<select
value={selectedCategory || ""}
onChange={(e) => setSelectedCategory(e.target.value || null)}
className="mcp-select"
style={selectStyles}>
<option value="">All Categories</option>
{categories.map((category) => (
<option key={category} value={category}>
{category}
</option>
))}
</select>
<select
value={sortBy}
onChange={(e) => setSortBy(e.target.value as "downloadCount" | "stars" | "name")}
className="mcp-select"
style={selectStyles}>
<option value="downloadCount">Sort by Downloads</option>
<option value="stars">Sort by Stars</option>
<option value="name">Sort by Name</option>
</select>
</div>
<VSCodeButton appearance="secondary" onClick={() => fetchMarketplace(true)} disabled={isRefreshing}>
<span className="codicon codicon-refresh" style={{ marginRight: "6px" }} />
Refresh
</VSCodeButton>
</div>
{items.length === 0 ? (
<style>
{`
.mcp-search-input:focus {
border-color: var(--vscode-focusBorder) !important;
}
.mcp-search-input:hover {
opacity: 0.9;
}
.mcp-select:focus {
border-color: var(--vscode-focusBorder) !important;
}
.mcp-select:hover {
opacity: 0.9;
}
`}
</style>
{filteredItems.length === 0 ? (
<div
style={{
display: "flex",
@ -105,10 +225,12 @@ const McpMarketplaceView = () => {
padding: "20px",
color: "var(--vscode-descriptionForeground)",
}}>
No MCP servers found in the marketplace
{searchQuery || selectedCategory
? "No matching MCP servers found"
: "No MCP servers found in the marketplace"}
</div>
) : (
items.map((item) => <McpMarketplaceCard key={item.mcpId} item={item} installedServers={mcpServers} />)
filteredItems.map((item) => <McpMarketplaceCard key={item.mcpId} item={item} installedServers={mcpServers} />)
)}
</div>
)