Skip to content

Commit c68e427

Browse files
celestial-vaultElephant Lumps
andauthored
migrate mcpMarketplaceCatalog (RooCodeInc#4023)
Co-authored-by: Elephant Lumps <[email protected]>
1 parent 0dca4de commit c68e427

File tree

6 files changed

+97
-45
lines changed

6 files changed

+97
-45
lines changed

proto/mcp.proto

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ service McpService {
1616
rpc toggleToolAutoApprove(ToggleToolAutoApproveRequest) returns (McpServers);
1717
rpc refreshMcpMarketplace(EmptyRequest) returns (McpMarketplaceCatalog);
1818
rpc openMcpSettings(EmptyRequest) returns (Empty);
19+
20+
// Subscribe to MCP marketplace catalog updates
21+
rpc subscribeToMcpMarketplaceCatalog(EmptyRequest) returns (stream McpMarketplaceCatalog);
1922
}
2023

2124
message ToggleMcpServerRequest {

src/core/controller/index.ts

Lines changed: 5 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ import { sendStateUpdate } from "./state/subscribeToState"
5656
import { sendAddToInputEvent } from "./ui/subscribeToAddToInput"
5757
import { sendAuthCallbackEvent } from "./account/subscribeToAuthCallback"
5858
import { sendChatButtonClickedEvent } from "./ui/subscribeToChatButtonClicked"
59+
import { sendMcpMarketplaceCatalogEvent } from "./mcp/subscribeToMcpMarketplaceCatalog"
5960
import { refreshClineRulesToggles } from "@core/context/instructions/user-instructions/cline-rules"
6061
import { refreshExternalRulesToggles } from "@core/context/instructions/user-instructions/external-rules"
6162
import { refreshWorkflowToggles } from "@core/context/instructions/user-instructions/workflows"
@@ -245,10 +246,7 @@ export class Controller {
245246

246247
getGlobalState(this.context, "mcpMarketplaceCatalog").then((mcpMarketplaceCatalog) => {
247248
if (mcpMarketplaceCatalog) {
248-
this.postMessageToWebview({
249-
type: "mcpMarketplaceCatalog",
250-
mcpMarketplaceCatalog: mcpMarketplaceCatalog as McpMarketplaceCatalog,
251-
})
249+
sendMcpMarketplaceCatalogEvent(mcpMarketplaceCatalog as McpMarketplaceCatalog)
252250
}
253251
})
254252
this.silentlyRefreshMcpMarketplace()
@@ -759,10 +757,6 @@ export class Controller {
759757
console.error("Failed to fetch MCP marketplace:", error)
760758
if (!silent) {
761759
const errorMessage = error instanceof Error ? error.message : "Failed to fetch MCP marketplace"
762-
await this.postMessageToWebview({
763-
type: "mcpMarketplaceCatalog",
764-
error: errorMessage,
765-
})
766760
vscode.window.showErrorMessage(errorMessage)
767761
}
768762
return undefined
@@ -808,10 +802,7 @@ export class Controller {
808802
try {
809803
const catalog = await this.fetchMcpMarketplaceFromApi(true)
810804
if (catalog) {
811-
await this.postMessageToWebview({
812-
type: "mcpMarketplaceCatalog",
813-
mcpMarketplaceCatalog: catalog,
814-
})
805+
await sendMcpMarketplaceCatalogEvent(catalog)
815806
}
816807
} catch (error) {
817808
console.error("Failed to silently refresh MCP marketplace:", error)
@@ -839,27 +830,17 @@ export class Controller {
839830
| McpMarketplaceCatalog
840831
| undefined
841832
if (!forceRefresh && cachedCatalog?.items) {
842-
await this.postMessageToWebview({
843-
type: "mcpMarketplaceCatalog",
844-
mcpMarketplaceCatalog: cachedCatalog,
845-
})
833+
await sendMcpMarketplaceCatalogEvent(cachedCatalog)
846834
return
847835
}
848836

849837
const catalog = await this.fetchMcpMarketplaceFromApi(false)
850838
if (catalog) {
851-
await this.postMessageToWebview({
852-
type: "mcpMarketplaceCatalog",
853-
mcpMarketplaceCatalog: catalog,
854-
})
839+
await sendMcpMarketplaceCatalogEvent(catalog)
855840
}
856841
} catch (error) {
857842
console.error("Failed to handle cached MCP marketplace:", error)
858843
const errorMessage = error instanceof Error ? error.message : "Failed to handle cached MCP marketplace"
859-
await this.postMessageToWebview({
860-
type: "mcpMarketplaceCatalog",
861-
error: errorMessage,
862-
})
863844
vscode.window.showErrorMessage(errorMessage)
864845
}
865846
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { Controller } from "../index"
2+
import { EmptyRequest } from "@shared/proto/common"
3+
import { McpMarketplaceCatalog } from "@shared/proto/mcp"
4+
import { StreamingResponseHandler, getRequestRegistry } from "../grpc-handler"
5+
6+
// Keep track of active subscriptions
7+
const activeMcpMarketplaceSubscriptions = new Set<StreamingResponseHandler>()
8+
9+
/**
10+
* Subscribe to MCP marketplace catalog updates
11+
* @param controller The controller instance
12+
* @param request The empty request
13+
* @param responseStream The streaming response handler
14+
* @param requestId The ID of the request (passed by the gRPC handler)
15+
*/
16+
export async function subscribeToMcpMarketplaceCatalog(
17+
controller: Controller,
18+
request: EmptyRequest,
19+
responseStream: StreamingResponseHandler,
20+
requestId?: string,
21+
): Promise<void> {
22+
// Add this subscription to the active subscriptions
23+
activeMcpMarketplaceSubscriptions.add(responseStream)
24+
25+
// Register cleanup when the connection is closed
26+
const cleanup = () => {
27+
activeMcpMarketplaceSubscriptions.delete(responseStream)
28+
}
29+
30+
// Register the cleanup function with the request registry if we have a requestId
31+
if (requestId) {
32+
getRequestRegistry().registerRequest(requestId, cleanup, { type: "mcp_marketplace_subscription" }, responseStream)
33+
}
34+
}
35+
36+
/**
37+
* Send an MCP marketplace catalog event to all active subscribers
38+
*/
39+
export async function sendMcpMarketplaceCatalogEvent(catalog: McpMarketplaceCatalog): Promise<void> {
40+
// Send the event to all active subscribers
41+
const promises = Array.from(activeMcpMarketplaceSubscriptions).map(async (responseStream) => {
42+
try {
43+
await responseStream(
44+
catalog,
45+
false, // Not the last message
46+
)
47+
} catch (error) {
48+
console.error("Error sending MCP marketplace catalog event:", error)
49+
// Remove the subscription if there was an error
50+
activeMcpMarketplaceSubscriptions.delete(responseStream)
51+
}
52+
})
53+
54+
await Promise.all(promises)
55+
}

src/shared/ExtensionMessage.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ export interface ExtensionMessage {
2626
| "requestyModels"
2727
| "mcpServers"
2828
| "relinquishControl"
29-
| "mcpMarketplaceCatalog"
3029
| "mcpDownloadDetails"
3130
| "commitSearchResults"
3231
| "openGraphData"

webview-ui/src/components/mcp/configuration/tabs/marketplace/McpMarketplaceView.tsx

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,16 @@ import { vscode } from "@/utils/vscode"
1414
import McpMarketplaceCard from "./McpMarketplaceCard"
1515
import McpSubmitCard from "./McpSubmitCard"
1616
const McpMarketplaceView = () => {
17-
const { mcpServers } = useExtensionState()
18-
const [items, setItems] = useState<McpMarketplaceItem[]>([])
17+
const { mcpServers, mcpMarketplaceCatalog } = useExtensionState()
1918
const [isLoading, setIsLoading] = useState(true)
2019
const [error, setError] = useState<string | null>(null)
2120
const [isRefreshing, setIsRefreshing] = useState(false)
2221
const [searchQuery, setSearchQuery] = useState("")
2322
const [selectedCategory, setSelectedCategory] = useState<string | null>(null)
2423
const [sortBy, setSortBy] = useState<"newest" | "stars" | "name" | "downloadCount">("downloadCount")
2524

25+
const items = mcpMarketplaceCatalog?.items || []
26+
2627
const categories = useMemo(() => {
2728
const uniqueCategories = new Set(items.map((item) => item.category))
2829
return Array.from(uniqueCategories).sort()
@@ -58,16 +59,7 @@ const McpMarketplaceView = () => {
5859
useEffect(() => {
5960
const handleMessage = (event: MessageEvent) => {
6061
const message = event.data
61-
if (message.type === "mcpMarketplaceCatalog") {
62-
if (message.error) {
63-
setError(message.error)
64-
} else {
65-
setItems(message.mcpMarketplaceCatalog?.items || [])
66-
setError(null)
67-
}
68-
setIsLoading(false)
69-
setIsRefreshing(false)
70-
} else if (message.type === "mcpDownloadDetails") {
62+
if (message.type === "mcpDownloadDetails") {
7163
if (message.error) {
7264
setError(message.error)
7365
}
@@ -76,14 +68,23 @@ const McpMarketplaceView = () => {
7668

7769
window.addEventListener("message", handleMessage)
7870

79-
// Fetch marketplace catalog
71+
// Fetch marketplace catalog on initial load
8072
fetchMarketplace()
8173

8274
return () => {
8375
window.removeEventListener("message", handleMessage)
8476
}
8577
}, [])
8678

79+
useEffect(() => {
80+
// Update loading state when catalog arrives
81+
if (mcpMarketplaceCatalog?.items) {
82+
setIsLoading(false)
83+
setIsRefreshing(false)
84+
setError(null)
85+
}
86+
}, [mcpMarketplaceCatalog])
87+
8788
const fetchMarketplace = (forceRefresh: boolean = false) => {
8889
if (forceRefresh) {
8990
setIsRefreshing(true)

webview-ui/src/context/ExtensionStateContext.tsx

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import {
1818
requestyDefaultModelInfo,
1919
} from "../../../src/shared/api"
2020
import { McpMarketplaceCatalog, McpServer, McpViewTab } from "../../../src/shared/mcp"
21-
import { ModelsServiceClient, StateServiceClient, UiServiceClient } from "../services/grpc-client"
21+
import { ModelsServiceClient, StateServiceClient, UiServiceClient, McpServiceClient } from "../services/grpc-client"
2222
import { convertTextMateToHljs } from "../utils/textMateToHljs"
2323
import { vscode } from "../utils/vscode"
2424

@@ -232,12 +232,6 @@ export const ExtensionStateContextProvider: React.FC<{
232232
setMcpServers(message.mcpServers ?? [])
233233
break
234234
}
235-
case "mcpMarketplaceCatalog": {
236-
if (message.mcpMarketplaceCatalog) {
237-
setMcpMarketplaceCatalog(message.mcpMarketplaceCatalog)
238-
}
239-
break
240-
}
241235
}
242236
}, [])
243237

@@ -251,6 +245,7 @@ export const ExtensionStateContextProvider: React.FC<{
251245
const accountButtonClickedSubscriptionRef = useRef<(() => void) | null>(null)
252246
const settingsButtonClickedSubscriptionRef = useRef<(() => void) | null>(null)
253247
const partialMessageUnsubscribeRef = useRef<(() => void) | null>(null)
248+
const mcpMarketplaceUnsubscribeRef = useRef<(() => void) | null>(null)
254249

255250
// Subscribe to state updates and UI events using the gRPC streaming API
256251
useEffect(() => {
@@ -436,6 +431,20 @@ export const ExtensionStateContextProvider: React.FC<{
436431
},
437432
})
438433

434+
// Subscribe to MCP marketplace catalog updates
435+
mcpMarketplaceUnsubscribeRef.current = McpServiceClient.subscribeToMcpMarketplaceCatalog(EmptyRequest.create({}), {
436+
onResponse: (catalog) => {
437+
console.log("[DEBUG] Received MCP marketplace catalog update from gRPC stream")
438+
setMcpMarketplaceCatalog(catalog)
439+
},
440+
onError: (error) => {
441+
console.error("Error in MCP marketplace catalog subscription:", error)
442+
},
443+
onComplete: () => {
444+
console.log("MCP marketplace catalog subscription completed")
445+
},
446+
})
447+
439448
// Still send the webviewDidLaunch message for other initialization
440449
vscode.postMessage({ type: "webviewDidLaunch" })
441450

@@ -484,6 +493,10 @@ export const ExtensionStateContextProvider: React.FC<{
484493
partialMessageUnsubscribeRef.current()
485494
partialMessageUnsubscribeRef.current = null
486495
}
496+
if (mcpMarketplaceUnsubscribeRef.current) {
497+
mcpMarketplaceUnsubscribeRef.current()
498+
mcpMarketplaceUnsubscribeRef.current = null
499+
}
487500
}
488501
}, [])
489502

0 commit comments

Comments
 (0)