diff --git a/proto/mcp.proto b/proto/mcp.proto index 4417695d89..e5155f4269 100644 --- a/proto/mcp.proto +++ b/proto/mcp.proto @@ -16,6 +16,9 @@ service McpService { rpc toggleToolAutoApprove(ToggleToolAutoApproveRequest) returns (McpServers); rpc refreshMcpMarketplace(EmptyRequest) returns (McpMarketplaceCatalog); rpc openMcpSettings(EmptyRequest) returns (Empty); + + // Subscribe to MCP marketplace catalog updates + rpc subscribeToMcpMarketplaceCatalog(EmptyRequest) returns (stream McpMarketplaceCatalog); } message ToggleMcpServerRequest { diff --git a/src/core/controller/index.ts b/src/core/controller/index.ts index de808d731d..27d2464ab4 100644 --- a/src/core/controller/index.ts +++ b/src/core/controller/index.ts @@ -56,6 +56,7 @@ import { sendStateUpdate } from "./state/subscribeToState" import { sendAddToInputEvent } from "./ui/subscribeToAddToInput" import { sendAuthCallbackEvent } from "./account/subscribeToAuthCallback" import { sendChatButtonClickedEvent } from "./ui/subscribeToChatButtonClicked" +import { sendMcpMarketplaceCatalogEvent } from "./mcp/subscribeToMcpMarketplaceCatalog" import { refreshClineRulesToggles } from "@core/context/instructions/user-instructions/cline-rules" import { refreshExternalRulesToggles } from "@core/context/instructions/user-instructions/external-rules" import { refreshWorkflowToggles } from "@core/context/instructions/user-instructions/workflows" @@ -243,10 +244,7 @@ export class Controller { getGlobalState(this.context, "mcpMarketplaceCatalog").then((mcpMarketplaceCatalog) => { if (mcpMarketplaceCatalog) { - this.postMessageToWebview({ - type: "mcpMarketplaceCatalog", - mcpMarketplaceCatalog: mcpMarketplaceCatalog as McpMarketplaceCatalog, - }) + sendMcpMarketplaceCatalogEvent(mcpMarketplaceCatalog as McpMarketplaceCatalog) } }) this.silentlyRefreshMcpMarketplace() @@ -741,10 +739,6 @@ export class Controller { console.error("Failed to fetch MCP marketplace:", error) if (!silent) { const errorMessage = error instanceof Error ? error.message : "Failed to fetch MCP marketplace" - await this.postMessageToWebview({ - type: "mcpMarketplaceCatalog", - error: errorMessage, - }) vscode.window.showErrorMessage(errorMessage) } return undefined @@ -790,10 +784,7 @@ export class Controller { try { const catalog = await this.fetchMcpMarketplaceFromApi(true) if (catalog) { - await this.postMessageToWebview({ - type: "mcpMarketplaceCatalog", - mcpMarketplaceCatalog: catalog, - }) + await sendMcpMarketplaceCatalogEvent(catalog) } } catch (error) { console.error("Failed to silently refresh MCP marketplace:", error) @@ -821,27 +812,17 @@ export class Controller { | McpMarketplaceCatalog | undefined if (!forceRefresh && cachedCatalog?.items) { - await this.postMessageToWebview({ - type: "mcpMarketplaceCatalog", - mcpMarketplaceCatalog: cachedCatalog, - }) + await sendMcpMarketplaceCatalogEvent(cachedCatalog) return } const catalog = await this.fetchMcpMarketplaceFromApi(false) if (catalog) { - await this.postMessageToWebview({ - type: "mcpMarketplaceCatalog", - mcpMarketplaceCatalog: catalog, - }) + await sendMcpMarketplaceCatalogEvent(catalog) } } catch (error) { console.error("Failed to handle cached MCP marketplace:", error) const errorMessage = error instanceof Error ? error.message : "Failed to handle cached MCP marketplace" - await this.postMessageToWebview({ - type: "mcpMarketplaceCatalog", - error: errorMessage, - }) vscode.window.showErrorMessage(errorMessage) } } diff --git a/src/core/controller/mcp/subscribeToMcpMarketplaceCatalog.ts b/src/core/controller/mcp/subscribeToMcpMarketplaceCatalog.ts new file mode 100644 index 0000000000..dca2606913 --- /dev/null +++ b/src/core/controller/mcp/subscribeToMcpMarketplaceCatalog.ts @@ -0,0 +1,55 @@ +import { Controller } from "../index" +import { EmptyRequest } from "@shared/proto/common" +import { McpMarketplaceCatalog } from "@shared/proto/mcp" +import { StreamingResponseHandler, getRequestRegistry } from "../grpc-handler" + +// Keep track of active subscriptions +const activeMcpMarketplaceSubscriptions = new Set() + +/** + * Subscribe to MCP marketplace catalog updates + * @param controller The controller instance + * @param request The empty request + * @param responseStream The streaming response handler + * @param requestId The ID of the request (passed by the gRPC handler) + */ +export async function subscribeToMcpMarketplaceCatalog( + controller: Controller, + request: EmptyRequest, + responseStream: StreamingResponseHandler, + requestId?: string, +): Promise { + // Add this subscription to the active subscriptions + activeMcpMarketplaceSubscriptions.add(responseStream) + + // Register cleanup when the connection is closed + const cleanup = () => { + activeMcpMarketplaceSubscriptions.delete(responseStream) + } + + // Register the cleanup function with the request registry if we have a requestId + if (requestId) { + getRequestRegistry().registerRequest(requestId, cleanup, { type: "mcp_marketplace_subscription" }, responseStream) + } +} + +/** + * Send an MCP marketplace catalog event to all active subscribers + */ +export async function sendMcpMarketplaceCatalogEvent(catalog: McpMarketplaceCatalog): Promise { + // Send the event to all active subscribers + const promises = Array.from(activeMcpMarketplaceSubscriptions).map(async (responseStream) => { + try { + await responseStream( + catalog, + false, // Not the last message + ) + } catch (error) { + console.error("Error sending MCP marketplace catalog event:", error) + // Remove the subscription if there was an error + activeMcpMarketplaceSubscriptions.delete(responseStream) + } + }) + + await Promise.all(promises) +} diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts index ea2663de21..40b65cb64a 100644 --- a/src/shared/ExtensionMessage.ts +++ b/src/shared/ExtensionMessage.ts @@ -27,7 +27,6 @@ export interface ExtensionMessage { | "requestyModels" | "mcpServers" | "relinquishControl" - | "mcpMarketplaceCatalog" | "mcpDownloadDetails" | "commitSearchResults" | "openGraphData" diff --git a/webview-ui/src/components/mcp/configuration/tabs/marketplace/McpMarketplaceView.tsx b/webview-ui/src/components/mcp/configuration/tabs/marketplace/McpMarketplaceView.tsx index b1afaf17a3..efeed8eb44 100644 --- a/webview-ui/src/components/mcp/configuration/tabs/marketplace/McpMarketplaceView.tsx +++ b/webview-ui/src/components/mcp/configuration/tabs/marketplace/McpMarketplaceView.tsx @@ -14,8 +14,7 @@ import { vscode } from "@/utils/vscode" import McpMarketplaceCard from "./McpMarketplaceCard" import McpSubmitCard from "./McpSubmitCard" const McpMarketplaceView = () => { - const { mcpServers } = useExtensionState() - const [items, setItems] = useState([]) + const { mcpServers, mcpMarketplaceCatalog } = useExtensionState() const [isLoading, setIsLoading] = useState(true) const [error, setError] = useState(null) const [isRefreshing, setIsRefreshing] = useState(false) @@ -23,6 +22,8 @@ const McpMarketplaceView = () => { const [selectedCategory, setSelectedCategory] = useState(null) const [sortBy, setSortBy] = useState<"newest" | "stars" | "name" | "downloadCount">("downloadCount") + const items = mcpMarketplaceCatalog?.items || [] + const categories = useMemo(() => { const uniqueCategories = new Set(items.map((item) => item.category)) return Array.from(uniqueCategories).sort() @@ -58,16 +59,7 @@ const McpMarketplaceView = () => { useEffect(() => { const handleMessage = (event: MessageEvent) => { const message = event.data - if (message.type === "mcpMarketplaceCatalog") { - if (message.error) { - setError(message.error) - } else { - setItems(message.mcpMarketplaceCatalog?.items || []) - setError(null) - } - setIsLoading(false) - setIsRefreshing(false) - } else if (message.type === "mcpDownloadDetails") { + if (message.type === "mcpDownloadDetails") { if (message.error) { setError(message.error) } @@ -76,7 +68,7 @@ const McpMarketplaceView = () => { window.addEventListener("message", handleMessage) - // Fetch marketplace catalog + // Fetch marketplace catalog on initial load fetchMarketplace() return () => { @@ -84,6 +76,15 @@ const McpMarketplaceView = () => { } }, []) + useEffect(() => { + // Update loading state when catalog arrives + if (mcpMarketplaceCatalog?.items) { + setIsLoading(false) + setIsRefreshing(false) + setError(null) + } + }, [mcpMarketplaceCatalog]) + const fetchMarketplace = (forceRefresh: boolean = false) => { if (forceRefresh) { setIsRefreshing(true) diff --git a/webview-ui/src/context/ExtensionStateContext.tsx b/webview-ui/src/context/ExtensionStateContext.tsx index c86435c5f3..d5f8830814 100644 --- a/webview-ui/src/context/ExtensionStateContext.tsx +++ b/webview-ui/src/context/ExtensionStateContext.tsx @@ -1,6 +1,6 @@ import React, { createContext, useCallback, useContext, useEffect, useRef, useState } from "react" import { useEvent } from "react-use" -import { StateServiceClient, ModelsServiceClient, UiServiceClient } from "../services/grpc-client" +import { StateServiceClient, ModelsServiceClient, UiServiceClient, McpServiceClient } from "../services/grpc-client" import { EmptyRequest } from "@shared/proto/common" import { WebviewProviderType as WebviewProviderTypeEnum, WebviewProviderTypeRequest } from "@shared/proto/ui" import { DEFAULT_AUTO_APPROVAL_SETTINGS } from "@shared/AutoApprovalSettings" @@ -252,12 +252,6 @@ export const ExtensionStateContextProvider: React.FC<{ setMcpServers(message.mcpServers ?? []) break } - case "mcpMarketplaceCatalog": { - if (message.mcpMarketplaceCatalog) { - setMcpMarketplaceCatalog(message.mcpMarketplaceCatalog) - } - break - } } }, []) @@ -268,6 +262,7 @@ export const ExtensionStateContextProvider: React.FC<{ const mcpButtonUnsubscribeRef = useRef<(() => void) | null>(null) const historyButtonClickedSubscriptionRef = useRef<(() => void) | null>(null) const chatButtonUnsubscribeRef = useRef<(() => void) | null>(null) + const mcpMarketplaceUnsubscribeRef = useRef<(() => void) | null>(null) // Subscribe to state updates and UI events using the gRPC streaming API useEffect(() => { @@ -397,6 +392,20 @@ export const ExtensionStateContextProvider: React.FC<{ onComplete: () => {}, }) + // Subscribe to MCP marketplace catalog updates + mcpMarketplaceUnsubscribeRef.current = McpServiceClient.subscribeToMcpMarketplaceCatalog(EmptyRequest.create({}), { + onResponse: (catalog) => { + console.log("[DEBUG] Received MCP marketplace catalog update from gRPC stream") + setMcpMarketplaceCatalog(catalog) + }, + onError: (error) => { + console.error("Error in MCP marketplace catalog subscription:", error) + }, + onComplete: () => { + console.log("MCP marketplace catalog subscription completed") + }, + }) + // Still send the webviewDidLaunch message for other initialization vscode.postMessage({ type: "webviewDidLaunch" }) @@ -418,6 +427,10 @@ export const ExtensionStateContextProvider: React.FC<{ chatButtonUnsubscribeRef.current() chatButtonUnsubscribeRef.current = null } + if (mcpMarketplaceUnsubscribeRef.current) { + mcpMarketplaceUnsubscribeRef.current() + mcpMarketplaceUnsubscribeRef.current = null + } } }, [])