Просмотр исходного кода

Cloud: add organization MCP controls (#6378)

- Add option to hide mcps from the marketplace
- Add ability to define new organization mcps
John Richmond 5 месяцев назад
Родитель
Сommit
6a42e91b15
28 измененных файлов с 379 добавлено и 40 удалено
  1. 6 0
      packages/cloud/src/CloudService.ts
  2. 4 0
      packages/types/src/cloud.ts
  3. 8 4
      src/core/webview/ClineProvider.ts
  4. 51 6
      src/services/marketplace/MarketplaceManager.ts
  5. 5 2
      src/services/marketplace/RemoteConfigLoader.ts
  6. 130 7
      src/services/marketplace/__tests__/MarketplaceManager.spec.ts
  7. 2 0
      src/shared/ExtensionMessage.ts
  8. 73 20
      webview-ui/src/components/marketplace/MarketplaceListView.tsx
  9. 26 1
      webview-ui/src/components/marketplace/MarketplaceViewStateManager.ts
  10. 2 0
      webview-ui/src/components/marketplace/__tests__/MarketplaceListView.spec.tsx
  11. 4 0
      webview-ui/src/i18n/locales/ca/marketplace.json
  12. 4 0
      webview-ui/src/i18n/locales/de/marketplace.json
  13. 4 0
      webview-ui/src/i18n/locales/en/marketplace.json
  14. 4 0
      webview-ui/src/i18n/locales/es/marketplace.json
  15. 4 0
      webview-ui/src/i18n/locales/fr/marketplace.json
  16. 4 0
      webview-ui/src/i18n/locales/hi/marketplace.json
  17. 4 0
      webview-ui/src/i18n/locales/id/marketplace.json
  18. 4 0
      webview-ui/src/i18n/locales/it/marketplace.json
  19. 4 0
      webview-ui/src/i18n/locales/ja/marketplace.json
  20. 4 0
      webview-ui/src/i18n/locales/ko/marketplace.json
  21. 4 0
      webview-ui/src/i18n/locales/nl/marketplace.json
  22. 4 0
      webview-ui/src/i18n/locales/pl/marketplace.json
  23. 4 0
      webview-ui/src/i18n/locales/pt-BR/marketplace.json
  24. 4 0
      webview-ui/src/i18n/locales/ru/marketplace.json
  25. 4 0
      webview-ui/src/i18n/locales/tr/marketplace.json
  26. 4 0
      webview-ui/src/i18n/locales/vi/marketplace.json
  27. 4 0
      webview-ui/src/i18n/locales/zh-CN/marketplace.json
  28. 4 0
      webview-ui/src/i18n/locales/zh-TW/marketplace.json

+ 6 - 0
packages/cloud/src/CloudService.ts

@@ -4,6 +4,7 @@ import type {
 	CloudUserInfo,
 	TelemetryEvent,
 	OrganizationAllowList,
+	OrganizationSettings,
 	ClineMessage,
 	ShareVisibility,
 } from "@roo-code/types"
@@ -174,6 +175,11 @@ export class CloudService {
 		return this.settingsService!.getAllowList()
 	}
 
+	public getOrganizationSettings(): OrganizationSettings | undefined {
+		this.ensureInitialized()
+		return this.settingsService!.getSettings()
+	}
+
 	// TelemetryClient
 
 	public captureEvent(event: TelemetryEvent): void {

+ 4 - 0
packages/types/src/cloud.ts

@@ -1,6 +1,7 @@
 import { z } from "zod"
 
 import { globalSettingsSchema } from "./global-settings.js"
+import { mcpMarketplaceItemSchema } from "./marketplace.js"
 
 /**
  * CloudUserInfo
@@ -110,6 +111,9 @@ export const organizationSettingsSchema = z.object({
 	cloudSettings: organizationCloudSettingsSchema.optional(),
 	defaultSettings: organizationDefaultSettingsSchema,
 	allowList: organizationAllowListSchema,
+	hiddenMcps: z.array(z.string()).optional(),
+	hideMarketplaceMcps: z.boolean().optional(),
+	mcps: z.array(mcpMarketplaceItemSchema).optional(),
 })
 
 export type OrganizationSettings = z.infer<typeof organizationSettingsSchema>

+ 8 - 4
src/core/webview/ClineProvider.ts

@@ -1328,10 +1328,10 @@ export class ClineProvider
 	 */
 	async fetchMarketplaceData() {
 		try {
-			const [marketplaceItems, marketplaceInstalledMetadata] = await Promise.all([
-				this.marketplaceManager.getCurrentItems().catch((error) => {
+			const [marketplaceResult, marketplaceInstalledMetadata] = await Promise.all([
+				this.marketplaceManager.getMarketplaceItems().catch((error) => {
 					console.error("Failed to fetch marketplace items:", error)
-					return [] as MarketplaceItem[]
+					return { organizationMcps: [], marketplaceItems: [], errors: [error.message] }
 				}),
 				this.marketplaceManager.getInstallationMetadata().catch((error) => {
 					console.error("Failed to fetch installation metadata:", error)
@@ -1342,16 +1342,20 @@ export class ClineProvider
 			// Send marketplace data separately
 			this.postMessageToWebview({
 				type: "marketplaceData",
-				marketplaceItems: marketplaceItems || [],
+				organizationMcps: marketplaceResult.organizationMcps || [],
+				marketplaceItems: marketplaceResult.marketplaceItems || [],
 				marketplaceInstalledMetadata: marketplaceInstalledMetadata || { project: {}, global: {} },
+				errors: marketplaceResult.errors,
 			})
 		} catch (error) {
 			console.error("Failed to fetch marketplace data:", error)
 			// Send empty data on error to prevent UI from hanging
 			this.postMessageToWebview({
 				type: "marketplaceData",
+				organizationMcps: [],
 				marketplaceItems: [],
 				marketplaceInstalledMetadata: { project: {}, global: {} },
+				errors: [error instanceof Error ? error.message : String(error)],
 			})
 
 			// Show user-friendly error notification for network issues

+ 51 - 6
src/services/marketplace/MarketplaceManager.ts

@@ -4,12 +4,19 @@ import * as path from "path"
 import * as yaml from "yaml"
 import { RemoteConfigLoader } from "./RemoteConfigLoader"
 import { SimpleInstaller } from "./SimpleInstaller"
-import type { MarketplaceItem, MarketplaceItemType } from "@roo-code/types"
+import type { MarketplaceItem, MarketplaceItemType, McpMarketplaceItem, OrganizationSettings } from "@roo-code/types"
 import { GlobalFileNames } from "../../shared/globalFileNames"
 import { ensureSettingsDirectoryExists } from "../../utils/globalContext"
 import { t } from "../../i18n"
 import { TelemetryService } from "@roo-code/telemetry"
 import type { CustomModesManager } from "../../core/config/CustomModesManager"
+import { CloudService } from "@roo-code/cloud"
+
+export interface MarketplaceItemsResponse {
+	organizationMcps: MarketplaceItem[]
+	marketplaceItems: MarketplaceItem[]
+	errors?: string[]
+}
 
 export class MarketplaceManager {
 	private configLoader: RemoteConfigLoader
@@ -23,17 +30,55 @@ export class MarketplaceManager {
 		this.installer = new SimpleInstaller(context, customModesManager)
 	}
 
-	async getMarketplaceItems(): Promise<{ items: MarketplaceItem[]; errors?: string[] }> {
+	async getMarketplaceItems(): Promise<MarketplaceItemsResponse> {
 		try {
-			const items = await this.configLoader.loadAllItems()
+			const errors: string[] = []
 
-			return { items }
+			let orgSettings: OrganizationSettings | undefined
+			try {
+				if (CloudService.hasInstance() && CloudService.instance.isAuthenticated()) {
+					orgSettings = CloudService.instance.getOrganizationSettings()
+				}
+			} catch (orgError) {
+				console.warn("Failed to load organization settings:", orgError)
+				const orgErrorMessage = orgError instanceof Error ? orgError.message : String(orgError)
+				errors.push(`Organization settings: ${orgErrorMessage}`)
+			}
+
+			const allMarketplaceItems = await this.configLoader.loadAllItems(orgSettings?.hideMarketplaceMcps)
+			let organizationMcps: MarketplaceItem[] = []
+			let marketplaceItems = allMarketplaceItems
+
+			if (orgSettings) {
+				if (orgSettings.mcps && orgSettings.mcps.length > 0) {
+					organizationMcps = orgSettings.mcps.map(
+						(mcp: McpMarketplaceItem): MarketplaceItem => ({
+							...mcp,
+							type: "mcp" as const,
+						}),
+					)
+				}
+
+				if (orgSettings.hiddenMcps && orgSettings.hiddenMcps.length > 0) {
+					const hiddenMcpIds = new Set(orgSettings.hiddenMcps)
+					marketplaceItems = allMarketplaceItems.filter(
+						(item) => item.type !== "mcp" || !hiddenMcpIds.has(item.id),
+					)
+				}
+			}
+
+			return {
+				organizationMcps,
+				marketplaceItems,
+				errors: errors.length > 0 ? errors : undefined,
+			}
 		} catch (error) {
 			const errorMessage = error instanceof Error ? error.message : String(error)
 			console.error("Failed to load marketplace items:", error)
 
 			return {
-				items: [],
+				organizationMcps: [],
+				marketplaceItems: [],
 				errors: [errorMessage],
 			}
 		}
@@ -41,7 +86,7 @@ export class MarketplaceManager {
 
 	async getCurrentItems(): Promise<MarketplaceItem[]> {
 		const result = await this.getMarketplaceItems()
-		return result.items
+		return [...result.organizationMcps, ...result.marketplaceItems]
 	}
 
 	filterItems(

+ 5 - 2
src/services/marketplace/RemoteConfigLoader.ts

@@ -23,10 +23,13 @@ export class RemoteConfigLoader {
 		this.apiBaseUrl = getRooCodeApiUrl()
 	}
 
-	async loadAllItems(): Promise<MarketplaceItem[]> {
+	async loadAllItems(hideMarketplaceMcps = false): Promise<MarketplaceItem[]> {
 		const items: MarketplaceItem[] = []
 
-		const [modes, mcps] = await Promise.all([this.fetchModes(), this.fetchMcps()])
+		const modesPromise = this.fetchModes()
+		const mcpsPromise = hideMarketplaceMcps ? Promise.resolve([]) : this.fetchMcps()
+
+		const [modes, mcps] = await Promise.all([modesPromise, mcpsPromise])
 
 		items.push(...modes, ...mcps)
 		return items

+ 130 - 7
src/services/marketplace/__tests__/MarketplaceManager.spec.ts

@@ -4,14 +4,21 @@ import type { MarketplaceItem } from "@roo-code/types"
 
 import { MarketplaceManager } from "../MarketplaceManager"
 
-// Mock axios
-vi.mock("axios")
-
-// Mock the cloud config
+// Mock CloudService
 vi.mock("@roo-code/cloud", () => ({
 	getRooCodeApiUrl: () => "https://test.api.com",
+	CloudService: {
+		hasInstance: vi.fn(),
+		instance: {
+			isAuthenticated: vi.fn(),
+			getOrganizationSettings: vi.fn(),
+		},
+	},
 }))
 
+// Mock axios
+vi.mock("axios")
+
 // Mock TelemetryService
 vi.mock("../../../../packages/telemetry/src/TelemetryService", () => ({
 	TelemetryService: {
@@ -165,8 +172,9 @@ describe("MarketplaceManager", () => {
 
 			const result = await manager.getMarketplaceItems()
 
-			expect(result.items).toHaveLength(1)
-			expect(result.items[0].name).toBe("Test Mode")
+			expect(result.marketplaceItems).toHaveLength(1)
+			expect(result.marketplaceItems[0].name).toBe("Test Mode")
+			expect(result.organizationMcps).toHaveLength(0)
 		})
 
 		it("should handle API errors gracefully", async () => {
@@ -175,9 +183,124 @@ describe("MarketplaceManager", () => {
 
 			const result = await manager.getMarketplaceItems()
 
-			expect(result.items).toHaveLength(0)
+			expect(result.marketplaceItems).toHaveLength(0)
+			expect(result.organizationMcps).toHaveLength(0)
 			expect(result.errors).toEqual(["API request failed"])
 		})
+
+		it("should return organization MCPs when available", async () => {
+			const { CloudService } = await import("@roo-code/cloud")
+
+			// Mock CloudService to return organization settings
+			vi.mocked(CloudService.hasInstance).mockReturnValue(true)
+			vi.mocked(CloudService.instance.isAuthenticated).mockReturnValue(true)
+			vi.mocked(CloudService.instance.getOrganizationSettings).mockReturnValue({
+				version: 1,
+				mcps: [
+					{
+						id: "org-mcp-1",
+						name: "Organization MCP",
+						description: "An organization MCP",
+						url: "https://example.com/org-mcp",
+						content: '{"command": "node", "args": ["org-server.js"]}',
+					},
+				],
+				hiddenMcps: [],
+				allowList: { allowAll: true, providers: {} },
+				defaultSettings: {},
+			})
+
+			// Mock the config loader to return test data
+			const mockItems: MarketplaceItem[] = [
+				{
+					id: "test-mcp",
+					name: "Test MCP",
+					description: "A test MCP",
+					type: "mcp",
+					url: "https://example.com/test-mcp",
+					content: '{"command": "node", "args": ["server.js"]}',
+				},
+			]
+
+			vi.spyOn(manager["configLoader"], "loadAllItems").mockResolvedValue(mockItems)
+
+			const result = await manager.getMarketplaceItems()
+
+			expect(result.organizationMcps).toHaveLength(1)
+			expect(result.organizationMcps[0].name).toBe("Organization MCP")
+			expect(result.marketplaceItems).toHaveLength(1)
+			expect(result.marketplaceItems[0].name).toBe("Test MCP")
+		})
+
+		it("should filter out hidden MCPs from marketplace results", async () => {
+			const { CloudService } = await import("@roo-code/cloud")
+
+			// Mock CloudService to return organization settings with hidden MCPs
+			vi.mocked(CloudService.hasInstance).mockReturnValue(true)
+			vi.mocked(CloudService.instance.isAuthenticated).mockReturnValue(true)
+			vi.mocked(CloudService.instance.getOrganizationSettings).mockReturnValue({
+				version: 1,
+				mcps: [],
+				hiddenMcps: ["hidden-mcp"],
+				allowList: { allowAll: true, providers: {} },
+				defaultSettings: {},
+			})
+
+			// Mock the config loader to return test data including a hidden MCP
+			const mockItems: MarketplaceItem[] = [
+				{
+					id: "visible-mcp",
+					name: "Visible MCP",
+					description: "A visible MCP",
+					type: "mcp",
+					url: "https://example.com/visible-mcp",
+					content: '{"command": "node", "args": ["visible.js"]}',
+				},
+				{
+					id: "hidden-mcp",
+					name: "Hidden MCP",
+					description: "A hidden MCP",
+					type: "mcp",
+					url: "https://example.com/hidden-mcp",
+					content: '{"command": "node", "args": ["hidden.js"]}',
+				},
+			]
+
+			vi.spyOn(manager["configLoader"], "loadAllItems").mockResolvedValue(mockItems)
+
+			const result = await manager.getMarketplaceItems()
+
+			expect(result.marketplaceItems).toHaveLength(1)
+			expect(result.marketplaceItems[0].name).toBe("Visible MCP")
+			expect(result.organizationMcps).toHaveLength(0)
+		})
+
+		it("should handle CloudService not being available", async () => {
+			const { CloudService } = await import("@roo-code/cloud")
+
+			// Mock CloudService to not be available
+			vi.mocked(CloudService.hasInstance).mockReturnValue(false)
+
+			// Mock the config loader to return test data
+			const mockItems: MarketplaceItem[] = [
+				{
+					id: "test-mcp",
+					name: "Test MCP",
+					description: "A test MCP",
+					type: "mcp",
+					url: "https://example.com/test-mcp",
+					content: '{"command": "node", "args": ["server.js"]}',
+				},
+			]
+
+			vi.spyOn(manager["configLoader"], "loadAllItems").mockResolvedValue(mockItems)
+
+			const result = await manager.getMarketplaceItems()
+
+			expect(result.organizationMcps).toHaveLength(0)
+			expect(result.marketplaceItems).toHaveLength(1)
+			expect(result.marketplaceItems[0].name).toBe("Test MCP")
+		})
 	})
 
 	describe("installMarketplaceItem", () => {

+ 2 - 0
src/shared/ExtensionMessage.ts

@@ -185,7 +185,9 @@ export interface ExtensionMessage {
 	organizationAllowList?: OrganizationAllowList
 	tab?: string
 	marketplaceItems?: MarketplaceItem[]
+	organizationMcps?: MarketplaceItem[]
 	marketplaceInstalledMetadata?: MarketplaceInstalledMetadata
+	errors?: string[]
 	visibility?: ShareVisibility
 	rulesFolderPath?: string
 	settings?: any

+ 73 - 20
webview-ui/src/components/marketplace/MarketplaceListView.tsx

@@ -21,12 +21,17 @@ export interface MarketplaceListViewProps {
 export function MarketplaceListView({ stateManager, allTags, filteredTags, filterByType }: MarketplaceListViewProps) {
 	const [state, manager] = useStateManager(stateManager)
 	const { t } = useAppTranslation()
-	const { marketplaceInstalledMetadata } = useExtensionState()
+	const { marketplaceInstalledMetadata, cloudUserInfo } = useExtensionState()
 	const [isTagPopoverOpen, setIsTagPopoverOpen] = React.useState(false)
 	const [tagSearch, setTagSearch] = React.useState("")
 	const allItems = state.displayItems || []
+	const organizationMcps = state.displayOrganizationMcps || []
+
+	// Filter items by type if specified
 	const items = filterByType ? allItems.filter((item) => item.type === filterByType) : allItems
-	const isEmpty = items.length === 0
+	const orgMcps = filterByType === "mcp" ? organizationMcps : []
+
+	const isEmpty = items.length === 0 && orgMcps.length === 0
 
 	return (
 		<>
@@ -193,24 +198,72 @@ export function MarketplaceListView({ stateManager, allTags, filteredTags, filte
 			)}
 
 			{!state.isFetching && !isEmpty && (
-				<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-1 xl:grid-cols-2 gap-3 pb-3">
-					{items.map((item) => (
-						<MarketplaceItemCard
-							key={item.id}
-							item={item}
-							filters={state.filters}
-							setFilters={(filters) =>
-								manager.transition({
-									type: "UPDATE_FILTERS",
-									payload: { filters },
-								})
-							}
-							installed={{
-								project: marketplaceInstalledMetadata?.project?.[item.id],
-								global: marketplaceInstalledMetadata?.global?.[item.id],
-							}}
-						/>
-					))}
+				<div className="pb-3">
+					{orgMcps.length > 0 && (
+						<div className="mb-6">
+							<div className="flex items-center gap-2 mb-3 px-1">
+								<span className="codicon codicon-organization text-lg"></span>
+								<h3 className="text-sm font-semibold text-vscode-foreground">
+									{t("marketplace:sections.organizationMcps", {
+										organization: cloudUserInfo?.organizationName,
+									})}
+								</h3>
+								<div className="flex-1 h-px bg-vscode-input-border"></div>
+							</div>
+							<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-1 xl:grid-cols-2 gap-3">
+								{orgMcps.map((item) => (
+									<MarketplaceItemCard
+										key={`org-${item.id}`}
+										item={item}
+										filters={state.filters}
+										setFilters={(filters) =>
+											manager.transition({
+												type: "UPDATE_FILTERS",
+												payload: { filters },
+											})
+										}
+										installed={{
+											project: marketplaceInstalledMetadata?.project?.[item.id],
+											global: marketplaceInstalledMetadata?.global?.[item.id],
+										}}
+									/>
+								))}
+							</div>
+						</div>
+					)}
+
+					{items.length > 0 && (
+						<div>
+							{orgMcps.length > 0 && (
+								<div className="flex items-center gap-2 mb-3 px-1">
+									<span className="codicon codicon-globe text-lg"></span>
+									<h3 className="text-sm font-semibold text-vscode-foreground">
+										{t("marketplace:sections.marketplace")}
+									</h3>
+									<div className="flex-1 h-px bg-vscode-input-border"></div>
+								</div>
+							)}
+							<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-1 xl:grid-cols-2 gap-3">
+								{items.map((item) => (
+									<MarketplaceItemCard
+										key={item.id}
+										item={item}
+										filters={state.filters}
+										setFilters={(filters) =>
+											manager.transition({
+												type: "UPDATE_FILTERS",
+												payload: { filters },
+											})
+										}
+										installed={{
+											project: marketplaceInstalledMetadata?.project?.[item.id],
+											global: marketplaceInstalledMetadata?.global?.[item.id],
+										}}
+									/>
+								))}
+							</div>
+						</div>
+					)}
 				</div>
 			)}
 

+ 26 - 1
webview-ui/src/components/marketplace/MarketplaceViewStateManager.ts

@@ -17,7 +17,9 @@ import { WebviewMessage } from "../../../../src/shared/WebviewMessage"
 
 export interface ViewState {
 	allItems: MarketplaceItem[]
+	organizationMcps: MarketplaceItem[]
 	displayItems?: MarketplaceItem[] // Items currently being displayed (filtered or all)
+	displayOrganizationMcps?: MarketplaceItem[] // Organization MCPs currently being displayed (filtered or all)
 	isFetching: boolean
 	activeTab: "mcp" | "mode"
 	filters: {
@@ -54,7 +56,9 @@ export class MarketplaceViewStateManager {
 	private getDefaultState(): ViewState {
 		return {
 			allItems: [],
+			organizationMcps: [],
 			displayItems: [], // Always initialize as empty array, not undefined
+			displayOrganizationMcps: [], // Always initialize as empty array, not undefined
 			isFetching: true, // Start with loading state for initial load
 			activeTab: "mcp",
 			filters: {
@@ -96,16 +100,22 @@ export class MarketplaceViewStateManager {
 	public getState(): ViewState {
 		// Only create new arrays if they exist and have items
 		const allItems = this.state.allItems.length ? [...this.state.allItems] : []
+		const organizationMcps = this.state.organizationMcps.length ? [...this.state.organizationMcps] : []
 		// Ensure displayItems is always an array, never undefined
 		// If displayItems is undefined or null, fall back to allItems
 		const displayItems = this.state.displayItems ? [...this.state.displayItems] : [...allItems]
+		const displayOrganizationMcps = this.state.displayOrganizationMcps
+			? [...this.state.displayOrganizationMcps]
+			: [...organizationMcps]
 		const tags = this.state.filters.tags.length ? [...this.state.filters.tags] : []
 
 		// Create minimal new state object
 		return {
 			...this.state,
 			allItems,
+			organizationMcps,
 			displayItems,
+			displayOrganizationMcps,
 			filters: {
 				...this.state.filters,
 				tags,
@@ -177,11 +187,14 @@ export class MarketplaceViewStateManager {
 
 				// Calculate display items based on current filters
 				let newDisplayItems: MarketplaceItem[]
+				let newDisplayOrganizationMcps: MarketplaceItem[]
 				if (this.isFilterActive()) {
 					newDisplayItems = this.filterItems([...items])
+					newDisplayOrganizationMcps = this.filterItems([...this.state.organizationMcps])
 				} else {
 					// No filters active - show all items
 					newDisplayItems = [...items]
+					newDisplayOrganizationMcps = [...this.state.organizationMcps]
 				}
 
 				// Update allItems as source of truth
@@ -189,6 +202,7 @@ export class MarketplaceViewStateManager {
 					...this.state,
 					allItems: [...items],
 					displayItems: newDisplayItems,
+					displayOrganizationMcps: newDisplayOrganizationMcps,
 					isFetching: false,
 				}
 
@@ -245,13 +259,15 @@ export class MarketplaceViewStateManager {
 					filters: updatedFilters,
 				}
 
-				// Apply filters to displayItems with the updated filters
+				// Apply filters to displayItems and displayOrganizationMcps with the updated filters
 				const newDisplayItems = this.filterItems(this.state.allItems)
+				const newDisplayOrganizationMcps = this.filterItems(this.state.organizationMcps)
 
 				// Update state with filtered items
 				this.state = {
 					...this.state,
 					displayItems: newDisplayItems,
+					displayOrganizationMcps: newDisplayOrganizationMcps,
 				}
 
 				// Send filter message
@@ -337,11 +353,14 @@ export class MarketplaceViewStateManager {
 				// If no filters are active, show all items
 				// If filters are active, apply filtering
 				let newDisplayItems: MarketplaceItem[]
+				let newDisplayOrganizationMcps: MarketplaceItem[]
 				if (this.isFilterActive()) {
 					newDisplayItems = this.filterItems(items)
+					newDisplayOrganizationMcps = this.filterItems(this.state.organizationMcps)
 				} else {
 					// No filters active - show all items
 					newDisplayItems = items
+					newDisplayOrganizationMcps = this.state.organizationMcps
 				}
 
 				// Update state in a single operation
@@ -350,6 +369,7 @@ export class MarketplaceViewStateManager {
 					isFetching: false,
 					allItems: items,
 					displayItems: newDisplayItems,
+					displayOrganizationMcps: newDisplayOrganizationMcps,
 				}
 				// Notification is handled below after all state parts are processed
 			}
@@ -390,19 +410,24 @@ export class MarketplaceViewStateManager {
 		// Handle marketplace data updates (fetched on demand)
 		if (message.type === "marketplaceData") {
 			const marketplaceItems = message.marketplaceItems
+			const organizationMcps = message.organizationMcps || []
 
 			if (marketplaceItems !== undefined) {
 				// Always use the marketplace items from the extension when they're provided
 				// This ensures fresh data is always displayed
 				const items = [...marketplaceItems]
+				const orgMcps = [...organizationMcps]
 				const newDisplayItems = this.isFilterActive() ? this.filterItems(items) : items
+				const newDisplayOrganizationMcps = this.isFilterActive() ? this.filterItems(orgMcps) : orgMcps
 
 				// Update state in a single operation
 				this.state = {
 					...this.state,
 					isFetching: false,
 					allItems: items,
+					organizationMcps: orgMcps,
 					displayItems: newDisplayItems,
+					displayOrganizationMcps: newDisplayOrganizationMcps,
 				}
 			}
 

+ 2 - 0
webview-ui/src/components/marketplace/__tests__/MarketplaceListView.spec.tsx

@@ -18,7 +18,9 @@ vi.mock("@/i18n/TranslationContext", () => ({
 const mockTransition = vi.fn()
 const mockState: ViewState = {
 	allItems: [],
+	organizationMcps: [],
 	displayItems: [],
+	displayOrganizationMcps: [],
 	isFetching: false,
 	activeTab: "mcp",
 	filters: {

+ 4 - 0
webview-ui/src/i18n/locales/ca/marketplace.json

@@ -35,6 +35,10 @@
 		},
 		"none": "Cap"
 	},
+	"sections": {
+		"organizationMcps": "MCPs de {{organization}}",
+		"marketplace": "Mercat"
+	},
 	"type-group": {
 		"modes": "Modes",
 		"mcps": "Servidors MCP"

+ 4 - 0
webview-ui/src/i18n/locales/de/marketplace.json

@@ -35,6 +35,10 @@
 		},
 		"none": "Keine"
 	},
+	"sections": {
+		"organizationMcps": "MCPs von {{organization}}",
+		"marketplace": "Marktplatz"
+	},
 	"type-group": {
 		"modes": "Modi",
 		"mcps": "MCP-Server"

+ 4 - 0
webview-ui/src/i18n/locales/en/marketplace.json

@@ -35,6 +35,10 @@
 		},
 		"none": "None"
 	},
+	"sections": {
+		"organizationMcps": "{{organization}} MCPs",
+		"marketplace": "Marketplace"
+	},
 	"type-group": {
 		"modes": "Modes",
 		"mcps": "MCP Servers"

+ 4 - 0
webview-ui/src/i18n/locales/es/marketplace.json

@@ -35,6 +35,10 @@
 		},
 		"none": "Ninguno"
 	},
+	"sections": {
+		"organizationMcps": "MCPs de {{organization}}",
+		"marketplace": "Mercado"
+	},
 	"type-group": {
 		"modes": "Modos",
 		"mcps": "Servidores MCP"

+ 4 - 0
webview-ui/src/i18n/locales/fr/marketplace.json

@@ -35,6 +35,10 @@
 		},
 		"none": "Aucun"
 	},
+	"sections": {
+		"organizationMcps": "MCPs de {{organization}}",
+		"marketplace": "Marché"
+	},
 	"type-group": {
 		"modes": "Modes",
 		"mcps": "Serveurs MCP"

+ 4 - 0
webview-ui/src/i18n/locales/hi/marketplace.json

@@ -35,6 +35,10 @@
 		},
 		"none": "कोई नहीं"
 	},
+	"sections": {
+		"organizationMcps": "{{organization}} MCPs",
+		"marketplace": "मार्केटप्लेस"
+	},
 	"type-group": {
 		"modes": "मोड",
 		"mcps": "MCP सर्वर"

+ 4 - 0
webview-ui/src/i18n/locales/id/marketplace.json

@@ -35,6 +35,10 @@
 		},
 		"none": "Tidak Ada"
 	},
+	"sections": {
+		"organizationMcps": "MCP {{organization}}",
+		"marketplace": "Marketplace"
+	},
 	"type-group": {
 		"modes": "Mode",
 		"mcps": "Server MCP"

+ 4 - 0
webview-ui/src/i18n/locales/it/marketplace.json

@@ -35,6 +35,10 @@
 		},
 		"none": "Nessuno"
 	},
+	"sections": {
+		"organizationMcps": "MCP di {{organization}}",
+		"marketplace": "Mercato"
+	},
 	"type-group": {
 		"modes": "Modalità",
 		"mcps": "Server MCP"

+ 4 - 0
webview-ui/src/i18n/locales/ja/marketplace.json

@@ -35,6 +35,10 @@
 		},
 		"none": "なし"
 	},
+	"sections": {
+		"organizationMcps": "{{organization}} MCPs",
+		"marketplace": "マーケットプレイス"
+	},
 	"type-group": {
 		"modes": "モード",
 		"mcps": "MCPサーバー"

+ 4 - 0
webview-ui/src/i18n/locales/ko/marketplace.json

@@ -35,6 +35,10 @@
 		},
 		"none": "없음"
 	},
+	"sections": {
+		"organizationMcps": "{{organization}} MCPs",
+		"marketplace": "마켓플레이스"
+	},
 	"type-group": {
 		"modes": "모드",
 		"mcps": "MCP 서버"

+ 4 - 0
webview-ui/src/i18n/locales/nl/marketplace.json

@@ -35,6 +35,10 @@
 		},
 		"none": "Geen"
 	},
+	"sections": {
+		"organizationMcps": "MCP's van {{organization}}",
+		"marketplace": "Marktplaats"
+	},
 	"type-group": {
 		"modes": "Modi",
 		"mcps": "MCP-servers"

+ 4 - 0
webview-ui/src/i18n/locales/pl/marketplace.json

@@ -35,6 +35,10 @@
 		},
 		"none": "Brak"
 	},
+	"sections": {
+		"organizationMcps": "MCPs {{organization}}",
+		"marketplace": "Rynek"
+	},
 	"type-group": {
 		"modes": "Tryby",
 		"mcps": "Serwery MCP"

+ 4 - 0
webview-ui/src/i18n/locales/pt-BR/marketplace.json

@@ -35,6 +35,10 @@
 		},
 		"none": "Nenhum"
 	},
+	"sections": {
+		"organizationMcps": "MCPs da {{organization}}",
+		"marketplace": "Marketplace"
+	},
 	"type-group": {
 		"modes": "Modos",
 		"mcps": "Servidores MCP"

+ 4 - 0
webview-ui/src/i18n/locales/ru/marketplace.json

@@ -35,6 +35,10 @@
 		},
 		"none": "Нет"
 	},
+	"sections": {
+		"organizationMcps": "MCPs {{organization}}",
+		"marketplace": "Маркетплейс"
+	},
 	"type-group": {
 		"modes": "Режимы",
 		"mcps": "MCP серверы"

+ 4 - 0
webview-ui/src/i18n/locales/tr/marketplace.json

@@ -35,6 +35,10 @@
 		},
 		"none": "Hiçbiri"
 	},
+	"sections": {
+		"organizationMcps": "{{organization}} MCP'leri",
+		"marketplace": "Marketplace"
+	},
 	"type-group": {
 		"modes": "Modlar",
 		"mcps": "MCP Sunucuları"

+ 4 - 0
webview-ui/src/i18n/locales/vi/marketplace.json

@@ -35,6 +35,10 @@
 		},
 		"none": "Không có"
 	},
+	"sections": {
+		"organizationMcps": "MCP của {{organization}}",
+		"marketplace": "Marketplace"
+	},
 	"type-group": {
 		"modes": "Chế độ",
 		"mcps": "Máy chủ MCP"

+ 4 - 0
webview-ui/src/i18n/locales/zh-CN/marketplace.json

@@ -35,6 +35,10 @@
 		},
 		"none": "无"
 	},
+	"sections": {
+		"organizationMcps": "{{organization}} MCPs",
+		"marketplace": "Marketplace"
+	},
 	"type-group": {
 		"modes": "模式",
 		"mcps": "MCP 服务"

+ 4 - 0
webview-ui/src/i18n/locales/zh-TW/marketplace.json

@@ -35,6 +35,10 @@
 		},
 		"none": "無"
 	},
+	"sections": {
+		"organizationMcps": "{{organization}} MCPs",
+		"marketplace": "Marketplace"
+	},
 	"type-group": {
 		"modes": "模式",
 		"mcps": "MCP 伺服器"