mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-07 08:26:51 +00:00
Cloud: add organization MCP controls (#6378)
- Add option to hide mcps from the marketplace - Add ability to define new organization mcps
This commit is contained in:
parent
43021d33e2
commit
6a42e91b15
28 changed files with 379 additions and 40 deletions
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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", () => {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
webview-ui/src/i18n/locales/ca/marketplace.json
generated
4
webview-ui/src/i18n/locales/ca/marketplace.json
generated
|
|
@ -35,6 +35,10 @@
|
|||
},
|
||||
"none": "Cap"
|
||||
},
|
||||
"sections": {
|
||||
"organizationMcps": "MCPs de {{organization}}",
|
||||
"marketplace": "Mercat"
|
||||
},
|
||||
"type-group": {
|
||||
"modes": "Modes",
|
||||
"mcps": "Servidors MCP"
|
||||
|
|
|
|||
4
webview-ui/src/i18n/locales/de/marketplace.json
generated
4
webview-ui/src/i18n/locales/de/marketplace.json
generated
|
|
@ -35,6 +35,10 @@
|
|||
},
|
||||
"none": "Keine"
|
||||
},
|
||||
"sections": {
|
||||
"organizationMcps": "MCPs von {{organization}}",
|
||||
"marketplace": "Marktplatz"
|
||||
},
|
||||
"type-group": {
|
||||
"modes": "Modi",
|
||||
"mcps": "MCP-Server"
|
||||
|
|
|
|||
|
|
@ -35,6 +35,10 @@
|
|||
},
|
||||
"none": "None"
|
||||
},
|
||||
"sections": {
|
||||
"organizationMcps": "{{organization}} MCPs",
|
||||
"marketplace": "Marketplace"
|
||||
},
|
||||
"type-group": {
|
||||
"modes": "Modes",
|
||||
"mcps": "MCP Servers"
|
||||
|
|
|
|||
4
webview-ui/src/i18n/locales/es/marketplace.json
generated
4
webview-ui/src/i18n/locales/es/marketplace.json
generated
|
|
@ -35,6 +35,10 @@
|
|||
},
|
||||
"none": "Ninguno"
|
||||
},
|
||||
"sections": {
|
||||
"organizationMcps": "MCPs de {{organization}}",
|
||||
"marketplace": "Mercado"
|
||||
},
|
||||
"type-group": {
|
||||
"modes": "Modos",
|
||||
"mcps": "Servidores MCP"
|
||||
|
|
|
|||
4
webview-ui/src/i18n/locales/fr/marketplace.json
generated
4
webview-ui/src/i18n/locales/fr/marketplace.json
generated
|
|
@ -35,6 +35,10 @@
|
|||
},
|
||||
"none": "Aucun"
|
||||
},
|
||||
"sections": {
|
||||
"organizationMcps": "MCPs de {{organization}}",
|
||||
"marketplace": "Marché"
|
||||
},
|
||||
"type-group": {
|
||||
"modes": "Modes",
|
||||
"mcps": "Serveurs MCP"
|
||||
|
|
|
|||
4
webview-ui/src/i18n/locales/hi/marketplace.json
generated
4
webview-ui/src/i18n/locales/hi/marketplace.json
generated
|
|
@ -35,6 +35,10 @@
|
|||
},
|
||||
"none": "कोई नहीं"
|
||||
},
|
||||
"sections": {
|
||||
"organizationMcps": "{{organization}} MCPs",
|
||||
"marketplace": "मार्केटप्लेस"
|
||||
},
|
||||
"type-group": {
|
||||
"modes": "मोड",
|
||||
"mcps": "MCP सर्वर"
|
||||
|
|
|
|||
4
webview-ui/src/i18n/locales/id/marketplace.json
generated
4
webview-ui/src/i18n/locales/id/marketplace.json
generated
|
|
@ -35,6 +35,10 @@
|
|||
},
|
||||
"none": "Tidak Ada"
|
||||
},
|
||||
"sections": {
|
||||
"organizationMcps": "MCP {{organization}}",
|
||||
"marketplace": "Marketplace"
|
||||
},
|
||||
"type-group": {
|
||||
"modes": "Mode",
|
||||
"mcps": "Server MCP"
|
||||
|
|
|
|||
4
webview-ui/src/i18n/locales/it/marketplace.json
generated
4
webview-ui/src/i18n/locales/it/marketplace.json
generated
|
|
@ -35,6 +35,10 @@
|
|||
},
|
||||
"none": "Nessuno"
|
||||
},
|
||||
"sections": {
|
||||
"organizationMcps": "MCP di {{organization}}",
|
||||
"marketplace": "Mercato"
|
||||
},
|
||||
"type-group": {
|
||||
"modes": "Modalità",
|
||||
"mcps": "Server MCP"
|
||||
|
|
|
|||
4
webview-ui/src/i18n/locales/ja/marketplace.json
generated
4
webview-ui/src/i18n/locales/ja/marketplace.json
generated
|
|
@ -35,6 +35,10 @@
|
|||
},
|
||||
"none": "なし"
|
||||
},
|
||||
"sections": {
|
||||
"organizationMcps": "{{organization}} MCPs",
|
||||
"marketplace": "マーケットプレイス"
|
||||
},
|
||||
"type-group": {
|
||||
"modes": "モード",
|
||||
"mcps": "MCPサーバー"
|
||||
|
|
|
|||
4
webview-ui/src/i18n/locales/ko/marketplace.json
generated
4
webview-ui/src/i18n/locales/ko/marketplace.json
generated
|
|
@ -35,6 +35,10 @@
|
|||
},
|
||||
"none": "없음"
|
||||
},
|
||||
"sections": {
|
||||
"organizationMcps": "{{organization}} MCPs",
|
||||
"marketplace": "마켓플레이스"
|
||||
},
|
||||
"type-group": {
|
||||
"modes": "모드",
|
||||
"mcps": "MCP 서버"
|
||||
|
|
|
|||
4
webview-ui/src/i18n/locales/nl/marketplace.json
generated
4
webview-ui/src/i18n/locales/nl/marketplace.json
generated
|
|
@ -35,6 +35,10 @@
|
|||
},
|
||||
"none": "Geen"
|
||||
},
|
||||
"sections": {
|
||||
"organizationMcps": "MCP's van {{organization}}",
|
||||
"marketplace": "Marktplaats"
|
||||
},
|
||||
"type-group": {
|
||||
"modes": "Modi",
|
||||
"mcps": "MCP-servers"
|
||||
|
|
|
|||
4
webview-ui/src/i18n/locales/pl/marketplace.json
generated
4
webview-ui/src/i18n/locales/pl/marketplace.json
generated
|
|
@ -35,6 +35,10 @@
|
|||
},
|
||||
"none": "Brak"
|
||||
},
|
||||
"sections": {
|
||||
"organizationMcps": "MCPs {{organization}}",
|
||||
"marketplace": "Rynek"
|
||||
},
|
||||
"type-group": {
|
||||
"modes": "Tryby",
|
||||
"mcps": "Serwery MCP"
|
||||
|
|
|
|||
|
|
@ -35,6 +35,10 @@
|
|||
},
|
||||
"none": "Nenhum"
|
||||
},
|
||||
"sections": {
|
||||
"organizationMcps": "MCPs da {{organization}}",
|
||||
"marketplace": "Marketplace"
|
||||
},
|
||||
"type-group": {
|
||||
"modes": "Modos",
|
||||
"mcps": "Servidores MCP"
|
||||
|
|
|
|||
4
webview-ui/src/i18n/locales/ru/marketplace.json
generated
4
webview-ui/src/i18n/locales/ru/marketplace.json
generated
|
|
@ -35,6 +35,10 @@
|
|||
},
|
||||
"none": "Нет"
|
||||
},
|
||||
"sections": {
|
||||
"organizationMcps": "MCPs {{organization}}",
|
||||
"marketplace": "Маркетплейс"
|
||||
},
|
||||
"type-group": {
|
||||
"modes": "Режимы",
|
||||
"mcps": "MCP серверы"
|
||||
|
|
|
|||
4
webview-ui/src/i18n/locales/tr/marketplace.json
generated
4
webview-ui/src/i18n/locales/tr/marketplace.json
generated
|
|
@ -35,6 +35,10 @@
|
|||
},
|
||||
"none": "Hiçbiri"
|
||||
},
|
||||
"sections": {
|
||||
"organizationMcps": "{{organization}} MCP'leri",
|
||||
"marketplace": "Marketplace"
|
||||
},
|
||||
"type-group": {
|
||||
"modes": "Modlar",
|
||||
"mcps": "MCP Sunucuları"
|
||||
|
|
|
|||
4
webview-ui/src/i18n/locales/vi/marketplace.json
generated
4
webview-ui/src/i18n/locales/vi/marketplace.json
generated
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -35,6 +35,10 @@
|
|||
},
|
||||
"none": "无"
|
||||
},
|
||||
"sections": {
|
||||
"organizationMcps": "{{organization}} MCPs",
|
||||
"marketplace": "Marketplace"
|
||||
},
|
||||
"type-group": {
|
||||
"modes": "模式",
|
||||
"mcps": "MCP 服务"
|
||||
|
|
|
|||
|
|
@ -35,6 +35,10 @@
|
|||
},
|
||||
"none": "無"
|
||||
},
|
||||
"sections": {
|
||||
"organizationMcps": "{{organization}} MCPs",
|
||||
"marketplace": "Marketplace"
|
||||
},
|
||||
"type-group": {
|
||||
"modes": "模式",
|
||||
"mcps": "MCP 伺服器"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue