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:
John Richmond 2025-07-29 14:11:51 -07:00 committed by GitHub
parent 43021d33e2
commit 6a42e91b15
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
28 changed files with 379 additions and 40 deletions

View file

@ -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 {

View file

@ -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>

View file

@ -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

View file

@ -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(

View file

@ -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

View file

@ -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", () => {

View file

@ -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

View file

@ -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>
)}

View file

@ -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,
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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"

View file

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

View file

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