From d47477902338bd6fc0ff33733d842121d451b7b7 Mon Sep 17 00:00:00 2001 From: axb Date: Fri, 20 Jun 2025 11:16:43 +0800 Subject: [PATCH 1/5] Support specifying available and unavailable MCP servers in custom mode --- packages/types/src/mode.ts | 17 ++ src/core/prompts/sections/mcp-servers.ts | 67 ++++- src/core/prompts/system.ts | 2 +- .../src/components/modes/McpSelector.tsx | 256 ++++++++++++++++++ webview-ui/src/components/modes/ModesView.tsx | 167 +++++++++--- webview-ui/src/i18n/locales/ca/prompts.json | 14 +- webview-ui/src/i18n/locales/de/prompts.json | 14 +- webview-ui/src/i18n/locales/en/prompts.json | 14 +- webview-ui/src/i18n/locales/es/prompts.json | 14 +- webview-ui/src/i18n/locales/fr/prompts.json | 14 +- webview-ui/src/i18n/locales/hi/prompts.json | 14 +- webview-ui/src/i18n/locales/id/prompts.json | 14 +- webview-ui/src/i18n/locales/it/prompts.json | 14 +- webview-ui/src/i18n/locales/ja/prompts.json | 14 +- webview-ui/src/i18n/locales/ko/prompts.json | 14 +- webview-ui/src/i18n/locales/nl/prompts.json | 14 +- webview-ui/src/i18n/locales/pl/prompts.json | 14 +- .../src/i18n/locales/pt-BR/prompts.json | 14 +- webview-ui/src/i18n/locales/ru/prompts.json | 14 +- webview-ui/src/i18n/locales/tr/prompts.json | 14 +- webview-ui/src/i18n/locales/vi/prompts.json | 14 +- .../src/i18n/locales/zh-CN/prompts.json | 14 +- .../src/i18n/locales/zh-TW/prompts.json | 14 +- 23 files changed, 693 insertions(+), 68 deletions(-) create mode 100644 webview-ui/src/components/modes/McpSelector.tsx diff --git a/packages/types/src/mode.ts b/packages/types/src/mode.ts index 88dcbb9574..d4cb347189 100644 --- a/packages/types/src/mode.ts +++ b/packages/types/src/mode.ts @@ -26,6 +26,23 @@ export const groupOptionsSchema = z.object({ { message: "Invalid regular expression pattern" }, ), description: z.string().optional(), + mcp: z + .object({ + included: z.array( + z.union([ + z.string(), + z.record( + z.string(), + z.object({ + allowedTools: z.array(z.string()).optional(), // not used yet + disallowedTools: z.array(z.string()).optional(), // not used yet + }), + ), + ]), + ), + description: z.string().optional(), + }) + .optional(), }) export type GroupOptions = z.infer diff --git a/src/core/prompts/sections/mcp-servers.ts b/src/core/prompts/sections/mcp-servers.ts index 643233ab6f..f5eb4d90f2 100644 --- a/src/core/prompts/sections/mcp-servers.ts +++ b/src/core/prompts/sections/mcp-servers.ts @@ -1,20 +1,79 @@ import { DiffStrategy } from "../../../shared/tools" import { McpHub } from "../../../services/mcp/McpHub" +import { GroupEntry, ModeConfig } from "@roo-code/types" +import { getGroupName } from "../../../shared/modes" +import { McpServer } from "../../../shared/mcp" + +let lastMcpHub: McpHub | undefined +let lastMcpIncludedList: string[] | undefined +let lastFilteredServers: McpServer[] = [] + +function memoizeFilteredServers(mcpHub: McpHub, mcpIncludedList?: string[]): McpServer[] { + const mcpHubChanged = mcpHub !== lastMcpHub + const listChanged = !areArraysEqual(mcpIncludedList, lastMcpIncludedList) + + if (!mcpHubChanged && !listChanged) { + return lastFilteredServers + } + + lastMcpHub = mcpHub + lastMcpIncludedList = mcpIncludedList + + lastFilteredServers = ( + mcpIncludedList && mcpIncludedList.length > 0 ? mcpHub.getAllServers() : mcpHub.getServers() + ).filter((server) => { + if (mcpIncludedList && mcpIncludedList.length > 0) { + return mcpIncludedList.includes(server.name) && server.status === "connected" + } + return server.status === "connected" + }) + + return lastFilteredServers +} +function areArraysEqual(arr1?: string[], arr2?: string[]): boolean { + if (!arr1 && !arr2) return true + if (!arr1 || !arr2) return false + if (arr1.length !== arr2.length) return false + + return arr1.every((item, index) => item === arr2[index]) +} export async function getMcpServersSection( mcpHub?: McpHub, diffStrategy?: DiffStrategy, enableMcpServerCreation?: boolean, + currentMode?: ModeConfig, ): Promise { if (!mcpHub) { return "" } + // Get MCP configuration for current mode + let mcpIncludedList: string[] | undefined + + if (currentMode) { + // Find MCP group configuration + const mcpGroup = currentMode.groups.find((group: GroupEntry) => { + if (Array.isArray(group) && group.length === 2 && group[0] === "mcp") { + return true + } + return getGroupName(group) === "mcp" + }) + + // If MCP group configuration is found, get mcpIncludedList from mcp.included + if (mcpGroup && Array.isArray(mcpGroup) && mcpGroup.length === 2) { + const options = mcpGroup[1] as { mcp?: { included?: unknown[] } } + mcpIncludedList = Array.isArray(options.mcp?.included) + ? options.mcp.included.filter((item: unknown): item is string => typeof item === "string") + : undefined + } + } + + const filteredServers = memoizeFilteredServers(mcpHub, mcpIncludedList) + const connectedServers = - mcpHub.getServers().length > 0 - ? `${mcpHub - .getServers() - .filter((server) => server.status === "connected") + filteredServers.length > 0 + ? `${filteredServers .map((server) => { const tools = server.tools ?.filter((tool) => tool.enabledForPrompt !== false) diff --git a/src/core/prompts/system.ts b/src/core/prompts/system.ts index 4ed1185da7..da8ad0a399 100644 --- a/src/core/prompts/system.ts +++ b/src/core/prompts/system.ts @@ -81,7 +81,7 @@ async function generatePrompt( const [modesSection, mcpServersSection] = await Promise.all([ getModesSection(context), shouldIncludeMcp - ? getMcpServersSection(mcpHub, effectiveDiffStrategy, enableMcpServerCreation) + ? getMcpServersSection(mcpHub, effectiveDiffStrategy, enableMcpServerCreation, modeConfig) : Promise.resolve(""), ]) diff --git a/webview-ui/src/components/modes/McpSelector.tsx b/webview-ui/src/components/modes/McpSelector.tsx new file mode 100644 index 0000000000..4baa3012fd --- /dev/null +++ b/webview-ui/src/components/modes/McpSelector.tsx @@ -0,0 +1,256 @@ +import React, { useState, useRef, useEffect } from "react" +import { ChevronsUpDown, X } from "lucide-react" +import { VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react" +import { + Popover, + PopoverContent, + PopoverTrigger, + Button, + Command, + CommandInput, + CommandList, + CommandEmpty, + CommandGroup, + CommandItem, +} from "@src/components/ui" +import { useAppTranslation } from "@src/i18n/TranslationContext" +import { ModeConfig, GroupEntry, GroupOptions } from "@roo-code/types" +import { McpServer } from "@roo/mcp" + +interface McpSelectorProps { + group: string + isEnabled: boolean + isCustomMode: boolean + mcpServers: McpServer[] + currentMode?: ModeConfig + visualMode: string + customModes: ModeConfig[] + findModeBySlug: (slug: string, modes: ModeConfig[]) => ModeConfig | undefined + updateCustomMode: (slug: string, config: ModeConfig) => void +} + +const McpSelector: React.FC = ({ + group, + isEnabled, + isCustomMode, + mcpServers, + currentMode, + visualMode, + customModes, + findModeBySlug, + updateCustomMode, +}) => { + const { t } = useAppTranslation() + + // State + const [isDialogOpen, setIsDialogOpen] = useState(false) + const [mcpIncludedList, setMcpIncludedList] = useState([]) + const [searchValue, setSearchValue] = useState("") + const searchInputRef = useRef(null) + + // Sync MCP settings + useEffect(() => { + if (!currentMode) { + setMcpIncludedList([]) + return + } + + const mcpGroupArr = currentMode.groups?.find( + (g: GroupEntry): g is ["mcp", GroupOptions] => Array.isArray(g) && g.length === 2 && g[0] === "mcp", + ) + + const rawGroupOptions: GroupOptions | undefined = mcpGroupArr ? mcpGroupArr[1] : undefined + + const included = Array.isArray(rawGroupOptions?.mcp?.included) + ? (rawGroupOptions.mcp.included.filter((item) => typeof item === "string") as string[]) + : [] + + // Sync MCP settings when mode changes + setMcpIncludedList(included) + }, [currentMode]) + // Handle save + function updateMcpGroupOptions(groups: GroupEntry[] = [], group: string, mcpIncludedList: string[]): GroupEntry[] { + let mcpGroupFound = false + const newGroups = groups + .map((g) => { + if (Array.isArray(g) && g[0] === group) { + mcpGroupFound = true + return [ + group, + { + ...(g[1] || {}), + mcp: mcpIncludedList.length > 0 ? { included: mcpIncludedList } : undefined, + }, + ] as GroupEntry + } + if (typeof g === "string" && g === group) { + mcpGroupFound = true + return [ + group, + { + mcp: mcpIncludedList.length > 0 ? { included: mcpIncludedList } : undefined, + }, + ] as GroupEntry + } + return g + }) + .filter((g) => g !== undefined) + + if (!mcpGroupFound && group === "mcp") { + const groupsWithoutSimpleMcp = newGroups.filter((g) => g !== "mcp") + groupsWithoutSimpleMcp.push([ + "mcp", + { + mcp: mcpIncludedList.length > 0 ? { included: mcpIncludedList } : undefined, + }, + ]) + return groupsWithoutSimpleMcp as GroupEntry[] + } else { + return newGroups as GroupEntry[] + } + } + + // Handle save + const handleSave = () => { + const customMode = findModeBySlug(visualMode, customModes) + if (!customMode) { + setIsDialogOpen(false) + return + } + + const updatedGroups = updateMcpGroupOptions(customMode.groups, group, mcpIncludedList) + + updateCustomMode(customMode.slug, { + ...customMode, + groups: updatedGroups, + source: customMode.source || "global", + }) + + setIsDialogOpen(false) + } + if (!isCustomMode || !isEnabled) { + return null + } + + return ( + { + setIsDialogOpen(open) + // Reset search box + if (!open) { + setTimeout(() => { + setSearchValue("") + }, 100) + } + }}> + + + + + +
+
{t("prompts:tools.selectMcpServers")}
+
+ + +
+
+
+ + {searchValue.length > 0 && ( +
+ { + setSearchValue("") + searchInputRef.current?.focus() + }} + /> +
+ )} +
+
+
+ {t("prompts:tools.requiredMcpList")} +
+
+ {t("prompts:tools.mcpDefaultDescription")} +
+ + + {mcpServers.length === 0 ? ( +
+ {t("prompts:tools.noMcpServers")} +
+ ) : ( +
{t("prompts:tools.noMatchFound")}
+ )} +
+ + {mcpServers + .filter( + (server) => + !searchValue || + server.name.toLowerCase().includes(searchValue.toLowerCase()), + ) + .map((server) => ( + { + const isIncluded = mcpIncludedList.includes(server.name) + if (isIncluded) { + setMcpIncludedList(mcpIncludedList.filter((n) => n !== server.name)) + } else { + setMcpIncludedList([...mcpIncludedList, server.name]) + } + }} + className="flex items-center px-2 py-1"> +
+ { + e.stopPropagation() + const isIncluded = mcpIncludedList.includes(server.name) + if (isIncluded) { + setMcpIncludedList( + mcpIncludedList.filter((n) => n !== server.name), + ) + } else { + setMcpIncludedList([...mcpIncludedList, server.name]) + } + }} + /> + {server.name} +
+
+ ))} +
+
+
+
+
+
+ ) +} + +export default McpSelector diff --git a/webview-ui/src/components/modes/ModesView.tsx b/webview-ui/src/components/modes/ModesView.tsx index d470f7a658..f7e4739fe1 100644 --- a/webview-ui/src/components/modes/ModesView.tsx +++ b/webview-ui/src/components/modes/ModesView.tsx @@ -49,6 +49,7 @@ import { } from "@src/components/ui" import { DeleteModeDialog } from "@src/components/modes/DeleteModeDialog" import { useEscapeKey } from "@src/hooks/useEscapeKey" +import McpSelector from "./McpSelector" // Get all available groups that should show in prompts view const availableGroups = (Object.keys(TOOL_GROUPS) as ToolGroup[]).filter((group) => !TOOL_GROUPS[group].alwaysAvailable) @@ -75,6 +76,7 @@ const ModesView = ({ onDone }: ModesViewProps) => { customInstructions, setCustomInstructions, customModes, + mcpServers, } = useExtensionState() // Use a local state to track the visually active mode @@ -1018,33 +1020,76 @@ const ModesView = ({ onDone }: ModesViewProps) => { ? customMode?.groups?.some((g) => getGroupName(g) === group) : currentMode?.groups?.some((g) => getGroupName(g) === group) + const isMcpGroup = group === "mcp" + + if (isMcpGroup) { + return ( +
+ + {t(`prompts:tools.toolNames.${group}`)} + + {isGroupEnabled && isCustomMode && ( + + )} +
+ ) + } + return ( - - {t(`prompts:tools.toolNames.${group}`)} - {group === "edit" && ( -
- {t("prompts:tools.allowedFiles")}{" "} - {(() => { - const currentMode = getCurrentMode() - const editGroup = currentMode?.groups?.find( - (g) => - Array.isArray(g) && - g[0] === "edit" && - g[1]?.fileRegex, - ) - if (!Array.isArray(editGroup)) return t("prompts:allFiles") - return ( - editGroup[1].description || - `/${editGroup[1].fileRegex}/` - ) - })()} -
- )} -
+
+ + {t(`prompts:tools.toolNames.${group}`)} + {group === "edit" && ( +
+ {t("prompts:tools.allowedFiles")}{" "} + {(() => { + const currentMode = getCurrentMode() + const editGroup = currentMode?.groups?.find( + (g) => + Array.isArray(g) && + g[0] === "edit" && + g[1]?.fileRegex, + ) + if (!Array.isArray(editGroup)) + return t("prompts:allFiles") + return ( + editGroup[1].description || + `/${editGroup[1].fileRegex}/` + ) + })()} +
+ )} +
+
) })} @@ -1494,25 +1539,57 @@ const ModesView = ({ onDone }: ModesViewProps) => { {t("prompts:createModeDialog.tools.description")}
- {availableGroups.map((group) => ( - getGroupName(g) === group)} - onChange={(e: Event | React.FormEvent) => { - const target = - (e as CustomEvent)?.detail?.target || (e.target as HTMLInputElement) - const checked = target.checked - if (checked) { - setNewModeGroups([...newModeGroups, group]) - } else { - setNewModeGroups( - newModeGroups.filter((g) => getGroupName(g) !== group), - ) - } - }}> - {t(`prompts:tools.toolNames.${group}`)} - - ))} + {availableGroups.map((group) => { + if (group === "mcp") { + return ( +
+ getGroupName(g) === group)} + onChange={(e: Event | React.FormEvent) => { + const target = + (e as CustomEvent)?.detail?.target || + (e.target as HTMLInputElement) + const checked = target.checked + if (checked) { + setNewModeGroups([...newModeGroups, group]) + } else { + setNewModeGroups( + newModeGroups.filter( + (g) => getGroupName(g) !== group, + ), + ) + } + }}> + {t(`prompts:tools.toolNames.${group}`)} + +
+ ) + } + return ( + getGroupName(g) === group)} + onChange={(e: Event | React.FormEvent) => { + const target = + (e as CustomEvent)?.detail?.target || + (e.target as HTMLInputElement) + const checked = target.checked + if (checked) { + setNewModeGroups([...newModeGroups, group]) + } else { + setNewModeGroups( + newModeGroups.filter((g) => getGroupName(g) !== group), + ) + } + }}> + {t(`prompts:tools.toolNames.${group}`)} + + ) + })}
{groupsError && (
{groupsError}
diff --git a/webview-ui/src/i18n/locales/ca/prompts.json b/webview-ui/src/i18n/locales/ca/prompts.json index 5730211daa..f041d7ea76 100644 --- a/webview-ui/src/i18n/locales/ca/prompts.json +++ b/webview-ui/src/i18n/locales/ca/prompts.json @@ -29,7 +29,19 @@ "command": "Executar comandes", "mcp": "Utilitzar MCP" }, - "noTools": "Cap" + "selectMcpServers": "Selecciona servidors MCP", + "searchMcpServers": "Cerca servidors MCP", + "requiredMcpList": "Servidors MCP requerits", + "noMcpServers": "No s'han trobat servidors MCP", + "noMatchFound": "No s'ha trobat cap coincidència", + "noTools": "Cap", + "mcpAll": "Tots", + "mcpSelectedCount": "{{included}} inclosos", + "mcpDefaultDescription": "Si no se selecciona cap servidor MCP, s'activaran tots per defecte.", + "buttons": { + "save": "Desa", + "clearAll": "Neteja-ho tot" + } }, "roleDefinition": { "title": "Definició de rol", diff --git a/webview-ui/src/i18n/locales/de/prompts.json b/webview-ui/src/i18n/locales/de/prompts.json index ce5fe66110..4f31fe3f77 100644 --- a/webview-ui/src/i18n/locales/de/prompts.json +++ b/webview-ui/src/i18n/locales/de/prompts.json @@ -29,7 +29,19 @@ "command": "Befehle ausführen", "mcp": "MCP verwenden" }, - "noTools": "Keine" + "selectMcpServers": "MCP-Server auswählen", + "searchMcpServers": "MCP-Server suchen", + "requiredMcpList": "Erforderliche MCP-Server", + "noMcpServers": "Keine MCP-Server gefunden", + "noMatchFound": "Keine Übereinstimmung gefunden", + "noTools": "Keine", + "mcpAll": "Alle", + "mcpSelectedCount": "{{included}} enthalten", + "mcpDefaultDescription": "Wenn kein MCP-Server ausgewählt ist, werden standardmäßig alle aktiviert.", + "buttons": { + "save": "Speichern", + "clearAll": "Alles löschen" + } }, "roleDefinition": { "title": "Rollendefinition", diff --git a/webview-ui/src/i18n/locales/en/prompts.json b/webview-ui/src/i18n/locales/en/prompts.json index 0ea5e133b8..a3b529626c 100644 --- a/webview-ui/src/i18n/locales/en/prompts.json +++ b/webview-ui/src/i18n/locales/en/prompts.json @@ -29,7 +29,19 @@ "command": "Run Commands", "mcp": "Use MCP" }, - "noTools": "None" + "selectMcpServers": "Select MCP Servers", + "searchMcpServers": "Search MCP Servers", + "requiredMcpList": "Required MCP Servers", + "noMcpServers": "No MCP servers found", + "noMatchFound": "No match found", + "noTools": "None", + "mcpAll": "All", + "mcpSelectedCount": "{{included}} Included", + "mcpDefaultDescription": "If no MCP server is selected, all will be enabled by default.", + "buttons": { + "save": "Save", + "clearAll": "Clear All" + } }, "roleDefinition": { "title": "Role Definition", diff --git a/webview-ui/src/i18n/locales/es/prompts.json b/webview-ui/src/i18n/locales/es/prompts.json index 26ee72b4b5..5918c8c388 100644 --- a/webview-ui/src/i18n/locales/es/prompts.json +++ b/webview-ui/src/i18n/locales/es/prompts.json @@ -29,7 +29,19 @@ "command": "Ejecutar comandos", "mcp": "Usar MCP" }, - "noTools": "Ninguna" + "selectMcpServers": "Seleccionar servidores MCP", + "searchMcpServers": "Buscar servidores MCP", + "requiredMcpList": "Servidores MCP requeridos", + "noMcpServers": "No se encontraron servidores MCP", + "noMatchFound": "No se encontraron coincidencias", + "noTools": "Ninguna", + "mcpAll": "Todos", + "mcpSelectedCount": "{{included}} incluidos", + "mcpDefaultDescription": "Si no se selecciona ningún servidor MCP, se habilitarán todos por defecto.", + "buttons": { + "save": "Guardar", + "clearAll": "Limpiar todo" + } }, "roleDefinition": { "title": "Definición de rol", diff --git a/webview-ui/src/i18n/locales/fr/prompts.json b/webview-ui/src/i18n/locales/fr/prompts.json index 3b43280c00..45e4a9c988 100644 --- a/webview-ui/src/i18n/locales/fr/prompts.json +++ b/webview-ui/src/i18n/locales/fr/prompts.json @@ -29,7 +29,19 @@ "command": "Exécuter des commandes", "mcp": "Utiliser MCP" }, - "noTools": "Aucun" + "selectMcpServers": "Sélectionner les serveurs MCP", + "searchMcpServers": "Rechercher les serveurs MCP", + "requiredMcpList": "Serveurs MCP requis", + "noMcpServers": "Aucun serveur MCP trouvé", + "noMatchFound": "Aucune correspondance trouvée", + "noTools": "Aucun", + "mcpAll": "Tous", + "mcpSelectedCount": "{{included}} inclus", + "mcpDefaultDescription": "Si aucun serveur MCP n'est sélectionné, tous seront activés par défaut.", + "buttons": { + "save": "Enregistrer", + "clearAll": "Tout effacer" + } }, "roleDefinition": { "title": "Définition du rôle", diff --git a/webview-ui/src/i18n/locales/hi/prompts.json b/webview-ui/src/i18n/locales/hi/prompts.json index bd403adde9..f9705a0d07 100644 --- a/webview-ui/src/i18n/locales/hi/prompts.json +++ b/webview-ui/src/i18n/locales/hi/prompts.json @@ -29,7 +29,19 @@ "command": "कमांड्स चलाएँ", "mcp": "MCP का उपयोग करें" }, - "noTools": "कोई नहीं" + "selectMcpServers": "MCP सर्वर चुनें", + "searchMcpServers": "MCP सर्वर खोजें", + "requiredMcpList": "आवश्यक MCP सर्वर", + "noMcpServers": "कोई MCP सर्वर नहीं मिला", + "noMatchFound": "कोई मिलान नहीं मिला", + "noTools": "कोई नहीं", + "mcpAll": "सभी", + "mcpSelectedCount": "{{included}} शामिल", + "mcpDefaultDescription": "यदि कोई MCP सर्वर चयनित नहीं है, तो सभी डिफ़ॉल्ट रूप से सक्षम होंगे।", + "buttons": { + "save": "सहेजें", + "clearAll": "सभी साफ़ करें" + } }, "roleDefinition": { "title": "भूमिका परिभाषा", diff --git a/webview-ui/src/i18n/locales/id/prompts.json b/webview-ui/src/i18n/locales/id/prompts.json index b4d45459f0..d4de99df7e 100644 --- a/webview-ui/src/i18n/locales/id/prompts.json +++ b/webview-ui/src/i18n/locales/id/prompts.json @@ -29,7 +29,19 @@ "command": "Jalankan Perintah", "mcp": "Gunakan MCP" }, - "noTools": "Tidak Ada" + "selectMcpServers": "Pilih server MCP", + "searchMcpServers": "Cari server MCP", + "requiredMcpList": "Server MCP yang Diperlukan", + "noMcpServers": "Tidak ada server MCP ditemukan", + "noMatchFound": "Tidak ada hasil cocok", + "noTools": "Tidak Ada", + "mcpAll": "Semua", + "mcpSelectedCount": "{{included}} termasuk", + "mcpDefaultDescription": "Jika tidak ada server MCP yang dipilih, semua akan diaktifkan secara default.", + "buttons": { + "save": "Simpan", + "clearAll": "Bersihkan semua" + } }, "roleDefinition": { "title": "Definisi Peran", diff --git a/webview-ui/src/i18n/locales/it/prompts.json b/webview-ui/src/i18n/locales/it/prompts.json index 57c144a650..7299f7eaa6 100644 --- a/webview-ui/src/i18n/locales/it/prompts.json +++ b/webview-ui/src/i18n/locales/it/prompts.json @@ -29,7 +29,19 @@ "command": "Esegui comandi", "mcp": "Usa MCP" }, - "noTools": "Nessuno" + "selectMcpServers": "Seleziona server MCP", + "searchMcpServers": "Cerca server MCP", + "requiredMcpList": "Server MCP richiesti", + "noMcpServers": "Nessun server MCP trovato", + "noMatchFound": "Nessuna corrispondenza trovata", + "noTools": "Nessuno", + "mcpAll": "Tutti", + "mcpSelectedCount": "{{included}} inclusi", + "mcpDefaultDescription": "Se non viene selezionato alcun server MCP, tutti saranno abilitati per impostazione predefinita.", + "buttons": { + "save": "Salva", + "clearAll": "Cancella tutto" + } }, "roleDefinition": { "title": "Definizione del ruolo", diff --git a/webview-ui/src/i18n/locales/ja/prompts.json b/webview-ui/src/i18n/locales/ja/prompts.json index 15cb64dcf2..b19a6178a7 100644 --- a/webview-ui/src/i18n/locales/ja/prompts.json +++ b/webview-ui/src/i18n/locales/ja/prompts.json @@ -29,7 +29,19 @@ "command": "コマンドを実行", "mcp": "MCP を使用" }, - "noTools": "なし" + "selectMcpServers": "MCPサーバーを選択", + "searchMcpServers": "MCPサーバーを検索", + "requiredMcpList": "必要な MCP サーバー", + "noMcpServers": "MCPサーバーが見つかりません", + "noMatchFound": "一致するものがありません", + "noTools": "なし", + "mcpAll": "すべて", + "mcpSelectedCount": "{{included}} 含まれる", + "mcpDefaultDescription": "MCP サーバーが選択されていない場合、すべてがデフォルトで有効になります。", + "buttons": { + "save": "保存", + "clearAll": "すべてクリア" + } }, "roleDefinition": { "title": "役割の定義", diff --git a/webview-ui/src/i18n/locales/ko/prompts.json b/webview-ui/src/i18n/locales/ko/prompts.json index d21107be43..4f2d0d74d2 100644 --- a/webview-ui/src/i18n/locales/ko/prompts.json +++ b/webview-ui/src/i18n/locales/ko/prompts.json @@ -29,7 +29,19 @@ "command": "명령 실행", "mcp": "MCP 사용" }, - "noTools": "없음" + "selectMcpServers": "MCP 서버 선택", + "searchMcpServers": "MCP 서버 검색", + "requiredMcpList": "필수 MCP 서버", + "noMcpServers": "MCP 서버를 찾을 수 없음", + "noMatchFound": "일치하는 항목 없음", + "noTools": "없음", + "mcpAll": "전체", + "mcpSelectedCount": "{{included}} 포함됨", + "mcpDefaultDescription": "MCP 서버를 선택하지 않으면 전체가 기본적으로 활성화됩니다.", + "buttons": { + "save": "저장", + "clearAll": "전체 해제" + } }, "roleDefinition": { "title": "역할 정의", diff --git a/webview-ui/src/i18n/locales/nl/prompts.json b/webview-ui/src/i18n/locales/nl/prompts.json index 43af1ada59..0300424571 100644 --- a/webview-ui/src/i18n/locales/nl/prompts.json +++ b/webview-ui/src/i18n/locales/nl/prompts.json @@ -29,7 +29,19 @@ "command": "Commando's uitvoeren", "mcp": "MCP gebruiken" }, - "noTools": "Geen" + "selectMcpServers": "Selecteer MCP-servers", + "searchMcpServers": "Zoek MCP-servers", + "requiredMcpList": "Vereiste MCP-servers", + "noMcpServers": "Geen MCP-servers gevonden", + "noMatchFound": "Geen overeenkomsten gevonden", + "noTools": "Geen", + "mcpAll": "Alle", + "mcpSelectedCount": "{{included}} inbegrepen", + "mcpDefaultDescription": "Als er geen MCP-server is geselecteerd, worden standaard alle ingeschakeld.", + "buttons": { + "save": "Opslaan", + "clearAll": "Alles wissen" + } }, "roleDefinition": { "title": "Roldefinitie", diff --git a/webview-ui/src/i18n/locales/pl/prompts.json b/webview-ui/src/i18n/locales/pl/prompts.json index 7bd71f49b0..2d495e7e85 100644 --- a/webview-ui/src/i18n/locales/pl/prompts.json +++ b/webview-ui/src/i18n/locales/pl/prompts.json @@ -29,7 +29,19 @@ "command": "Uruchamiaj polecenia", "mcp": "Używaj MCP" }, - "noTools": "Brak" + "selectMcpServers": "Wybierz serwery MCP", + "searchMcpServers": "Szukaj serwerów MCP", + "requiredMcpList": "Wymagane serwery MCP", + "noMcpServers": "Nie znaleziono serwerów MCP", + "noMatchFound": "Brak pasujących wyników", + "noTools": "Brak", + "mcpAll": "Wszystkie", + "mcpSelectedCount": "{{included}} uwzględnione", + "mcpDefaultDescription": "Jeśli nie wybrano żadnego serwera MCP, domyślnie wszystkie będą włączone.", + "buttons": { + "save": "Zapisz", + "clearAll": "Wyczyść wszystko" + } }, "roleDefinition": { "title": "Definicja roli", diff --git a/webview-ui/src/i18n/locales/pt-BR/prompts.json b/webview-ui/src/i18n/locales/pt-BR/prompts.json index 5bc3234c4d..7e2ccfc213 100644 --- a/webview-ui/src/i18n/locales/pt-BR/prompts.json +++ b/webview-ui/src/i18n/locales/pt-BR/prompts.json @@ -29,7 +29,19 @@ "command": "Executar comandos", "mcp": "Usar MCP" }, - "noTools": "Nenhuma" + "selectMcpServers": "Selecionar servidores MCP", + "searchMcpServers": "Buscar servidores MCP", + "requiredMcpList": "Servidores MCP necessários", + "noMcpServers": "Nenhum servidor MCP encontrado", + "noMatchFound": "Nenhuma correspondência encontrada", + "noTools": "Nenhuma", + "mcpAll": "Todos", + "mcpSelectedCount": "{{included}} incluídos", + "mcpDefaultDescription": "Se nenhum servidor MCP for selecionado, todos serão ativados por padrão.", + "buttons": { + "save": "Salvar", + "clearAll": "Limpar tudo" + } }, "roleDefinition": { "title": "Definição de função", diff --git a/webview-ui/src/i18n/locales/ru/prompts.json b/webview-ui/src/i18n/locales/ru/prompts.json index cc9210c678..fc5a7123ac 100644 --- a/webview-ui/src/i18n/locales/ru/prompts.json +++ b/webview-ui/src/i18n/locales/ru/prompts.json @@ -29,7 +29,19 @@ "command": "Выполнять команды", "mcp": "Использовать MCP" }, - "noTools": "Отсутствуют" + "selectMcpServers": "Выбери MCP-серверы", + "searchMcpServers": "Искать MCP-серверы", + "requiredMcpList": "Требуемые MCP-серверы", + "noMcpServers": "MCP-серверы не найдены", + "noMatchFound": "Совпадений не найдено", + "noTools": "Отсутствуют", + "mcpAll": "Все", + "mcpSelectedCount": "{{included}} включено", + "mcpDefaultDescription": "Если не выбран ни один MCP-сервер, по умолчанию будут активированы все.", + "buttons": { + "save": "Сохранить", + "clearAll": "Очистить всё" + } }, "roleDefinition": { "title": "Определение роли", diff --git a/webview-ui/src/i18n/locales/tr/prompts.json b/webview-ui/src/i18n/locales/tr/prompts.json index eec283977c..92695db395 100644 --- a/webview-ui/src/i18n/locales/tr/prompts.json +++ b/webview-ui/src/i18n/locales/tr/prompts.json @@ -29,7 +29,19 @@ "command": "Komutları Çalıştır", "mcp": "MCP Kullan" }, - "noTools": "Yok" + "selectMcpServers": "MCP sunucularını seç", + "searchMcpServers": "MCP sunucularını ara", + "requiredMcpList": "Gerekli MCP Sunucuları", + "noMcpServers": "MCP sunucusu bulunamadı", + "noMatchFound": "Eşleşme bulunamadı", + "noTools": "Yok", + "mcpAll": "Tümü", + "mcpSelectedCount": "{{included}} dahil", + "mcpDefaultDescription": "Hiçbir MCP sunucusu seçilmezse, varsayılan olarak tümü etkinleştirilir.", + "buttons": { + "save": "Kaydet", + "clearAll": "Tümünü temizle" + } }, "roleDefinition": { "title": "Rol Tanımı", diff --git a/webview-ui/src/i18n/locales/vi/prompts.json b/webview-ui/src/i18n/locales/vi/prompts.json index d7a38cda6f..d9179c6a43 100644 --- a/webview-ui/src/i18n/locales/vi/prompts.json +++ b/webview-ui/src/i18n/locales/vi/prompts.json @@ -29,7 +29,19 @@ "command": "Chạy lệnh", "mcp": "Sử dụng MCP" }, - "noTools": "Không có" + "selectMcpServers": "Chọn máy chủ MCP", + "searchMcpServers": "Tìm kiếm máy chủ MCP", + "requiredMcpList": "Máy chủ MCP cần thiết", + "noMcpServers": "Không tìm thấy máy chủ MCP", + "noMatchFound": "Không tìm thấy kết quả phù hợp", + "noTools": "Không có", + "mcpAll": "Tất cả", + "mcpSelectedCount": "{{included}} đã bao gồm", + "mcpDefaultDescription": "Nếu không chọn máy chủ MCP nào, tất cả sẽ được bật mặc định.", + "buttons": { + "save": "Lưu", + "clearAll": "Xóa tất cả" + } }, "roleDefinition": { "title": "Định nghĩa vai trò", diff --git a/webview-ui/src/i18n/locales/zh-CN/prompts.json b/webview-ui/src/i18n/locales/zh-CN/prompts.json index c21c22b7bd..45af8136ed 100644 --- a/webview-ui/src/i18n/locales/zh-CN/prompts.json +++ b/webview-ui/src/i18n/locales/zh-CN/prompts.json @@ -29,7 +29,19 @@ "command": "运行命令", "mcp": "MCP服务" }, - "noTools": "无" + "selectMcpServers": "选择 MCP 服务", + "searchMcpServers": "搜索 MCP 服务", + "requiredMcpList": "所需 MCP 服务", + "noMcpServers": "未发现 MCP 服务", + "noMatchFound": "无匹配结果", + "noTools": "无", + "mcpAll": "全部", + "mcpSelectedCount": "{{included}} 已包含", + "mcpDefaultDescription": "如果未选择任何 MCP 服务,将默认启用全部。", + "buttons": { + "save": "保存", + "clearAll": "清除全部" + } }, "roleDefinition": { "title": "角色定义", diff --git a/webview-ui/src/i18n/locales/zh-TW/prompts.json b/webview-ui/src/i18n/locales/zh-TW/prompts.json index 47a32d7d83..0687e8e091 100644 --- a/webview-ui/src/i18n/locales/zh-TW/prompts.json +++ b/webview-ui/src/i18n/locales/zh-TW/prompts.json @@ -29,7 +29,19 @@ "command": "執行命令", "mcp": "使用 MCP" }, - "noTools": "無" + "selectMcpServers": "選擇 MCP 伺服器", + "searchMcpServers": "搜尋 MCP 伺服器", + "requiredMcpList": "所需 MCP 伺服器", + "noMcpServers": "未找到 MCP 伺服器", + "noMatchFound": "沒有符合的結果", + "noTools": "無", + "mcpAll": "全部", + "mcpSelectedCount": "{{included}} 已包含", + "mcpDefaultDescription": "若未選擇任何 MCP 伺服器,則預設啟用全部。", + "buttons": { + "save": "儲存", + "clearAll": "清除全部" + } }, "roleDefinition": { "title": "角色定義", From 1aa827450c8a43fd452aa75741d335790d378d6f Mon Sep 17 00:00:00 2001 From: axb Date: Tue, 8 Jul 2025 01:21:48 +0800 Subject: [PATCH 2/5] show connected mcp servers --- packages/types/src/mode.ts | 13 +-- src/core/prompts/sections/mcp-servers.ts | 89 ++++++++++++------- .../src/components/modes/McpSelector.tsx | 81 +++++++++-------- 3 files changed, 100 insertions(+), 83 deletions(-) diff --git a/packages/types/src/mode.ts b/packages/types/src/mode.ts index d4cb347189..28c5b45927 100644 --- a/packages/types/src/mode.ts +++ b/packages/types/src/mode.ts @@ -28,18 +28,7 @@ export const groupOptionsSchema = z.object({ description: z.string().optional(), mcp: z .object({ - included: z.array( - z.union([ - z.string(), - z.record( - z.string(), - z.object({ - allowedTools: z.array(z.string()).optional(), // not used yet - disallowedTools: z.array(z.string()).optional(), // not used yet - }), - ), - ]), - ), + included: z.array(z.string()), description: z.string().optional(), }) .optional(), diff --git a/src/core/prompts/sections/mcp-servers.ts b/src/core/prompts/sections/mcp-servers.ts index f5eb4d90f2..62b3a5f6d6 100644 --- a/src/core/prompts/sections/mcp-servers.ts +++ b/src/core/prompts/sections/mcp-servers.ts @@ -71,42 +71,63 @@ export async function getMcpServersSection( const filteredServers = memoizeFilteredServers(mcpHub, mcpIncludedList) - const connectedServers = - filteredServers.length > 0 - ? `${filteredServers - .map((server) => { - const tools = server.tools + let connectedServers: string + + if (filteredServers.length > 0) { + connectedServers = `${filteredServers + .map((server) => { + const tools = server.tools ?.filter((tool) => tool.enabledForPrompt !== false) - ?.map((tool) => { - const schemaStr = tool.inputSchema - ? ` Input Schema: - ${JSON.stringify(tool.inputSchema, null, 2).split("\n").join("\n ")}` - : "" - - return `- ${tool.name}: ${tool.description}\n${schemaStr}` - }) - .join("\n\n") - - const templates = server.resourceTemplates - ?.map((template) => `- ${template.uriTemplate} (${template.name}): ${template.description}`) - .join("\n") - - const resources = server.resources - ?.map((resource) => `- ${resource.uri} (${resource.name}): ${resource.description}`) - .join("\n") - - const config = JSON.parse(server.config) - - return ( - `## ${server.name}${config.command ? ` (\`${config.command}${config.args && Array.isArray(config.args) ? ` ${config.args.join(" ")}` : ""}\`)` : ""}` + - (server.instructions ? `\n\n### Instructions\n${server.instructions}` : "") + - (tools ? `\n\n### Available Tools\n${tools}` : "") + - (templates ? `\n\n### Resource Templates\n${templates}` : "") + - (resources ? `\n\n### Direct Resources\n${resources}` : "") - ) + ?.map((tool) => { + const schemaStr = tool.inputSchema + ? ` Input Schema: + ${JSON.stringify(tool.inputSchema, null, 2).split("\n").join("\n ")}` + : "" + + return `- ${tool.name}: ${tool.description}\n${schemaStr}` }) - .join("\n\n")}` - : "(No MCP servers currently connected)" + .join("\n\n") + + const templates = server.resourceTemplates + ?.map((template) => `- ${template.uriTemplate} (${template.name}): ${template.description}`) + .join("\n") + + const resources = server.resources + ?.map((resource) => `- ${resource.uri} (${resource.name}): ${resource.description}`) + .join("\n") + + const config = JSON.parse(server.config) + + return ( + `## ${server.name}${config.command ? ` (\`${config.command}${config.args && Array.isArray(config.args) ? ` ${config.args.join(" ")}` : ""}\`)` : ""}` + + (server.instructions ? `\n\n### Instructions\n${server.instructions}` : "") + + (tools ? `\n\n### Available Tools\n${tools}` : "") + + (templates ? `\n\n### Resource Templates\n${templates}` : "") + + (resources ? `\n\n### Direct Resources\n${resources}` : "") + ) + }) + .join("\n\n")}` + } else if (mcpIncludedList && mcpIncludedList.length > 0) { + const allServers = mcpHub.getAllServers() + const disconnectedServers = mcpIncludedList + .map((name) => { + const server = allServers.find((s) => s.name === name) + if (server && server.status !== "connected") { + return `- ${server.name} (${server.status})` + } + if (!server) { + return `- ${name} (not found)` + } + return null + }) + .filter(Boolean) + .join("\n") + connectedServers = `(Configured MCP servers are not currently connected)${ + disconnectedServers ? `\n\nConfigured but disconnected servers:\n${disconnectedServers}` : "" + }` + } else { + connectedServers = "(No MCP servers currently connected)" + } const baseSection = `MCP SERVERS diff --git a/webview-ui/src/components/modes/McpSelector.tsx b/webview-ui/src/components/modes/McpSelector.tsx index 4baa3012fd..50ce0a1f74 100644 --- a/webview-ui/src/components/modes/McpSelector.tsx +++ b/webview-ui/src/components/modes/McpSelector.tsx @@ -206,44 +206,51 @@ const McpSelector: React.FC = ({ )} - {mcpServers - .filter( - (server) => - !searchValue || - server.name.toLowerCase().includes(searchValue.toLowerCase()), + {(() => { + const uniqueMcpServers = Array.from( + new Map(mcpServers.map((server) => [server.name, server])).values(), ) - .map((server) => ( - { - const isIncluded = mcpIncludedList.includes(server.name) - if (isIncluded) { - setMcpIncludedList(mcpIncludedList.filter((n) => n !== server.name)) - } else { - setMcpIncludedList([...mcpIncludedList, server.name]) - } - }} - className="flex items-center px-2 py-1"> -
- { - e.stopPropagation() - const isIncluded = mcpIncludedList.includes(server.name) - if (isIncluded) { - setMcpIncludedList( - mcpIncludedList.filter((n) => n !== server.name), - ) - } else { - setMcpIncludedList([...mcpIncludedList, server.name]) - } - }} - /> - {server.name} -
-
- ))} + return uniqueMcpServers + .filter( + (server) => + !searchValue || + server.name.toLowerCase().includes(searchValue.toLowerCase()), + ) + .map((server) => ( + { + const isIncluded = mcpIncludedList.includes(server.name) + if (isIncluded) { + setMcpIncludedList( + mcpIncludedList.filter((n) => n !== server.name), + ) + } else { + setMcpIncludedList([...mcpIncludedList, server.name]) + } + }} + className="flex items-center px-2 py-1"> +
+ { + e.stopPropagation() + const isIncluded = mcpIncludedList.includes(server.name) + if (isIncluded) { + setMcpIncludedList( + mcpIncludedList.filter((n) => n !== server.name), + ) + } else { + setMcpIncludedList([...mcpIncludedList, server.name]) + } + }} + /> + {server.name} +
+
+ )) + })()}
From 7e92618e43678a27db2f2ab703e47004ca6e37e7 Mon Sep 17 00:00:00 2001 From: Daniel Riccio Date: Tue, 5 Aug 2025 17:35:34 -0500 Subject: [PATCH 3/5] fix: update mock MCP server configuration in tests - Add status: 'connected' to mock MCP server - Add proper config object with transport details - Update test snapshots to reflect connected MCP server state - Fixes failing tests after MCP server filtering implementation --- .../mcp-server-creation-enabled.snap | 2 +- .../system-prompt/with-mcp-hub-provided.snap | 2 +- .../__tests__/add-custom-instructions.spec.ts | 31 ++++++++++++++++++- .../prompts/__tests__/system-prompt.spec.ts | 31 ++++++++++++++++++- 4 files changed, 62 insertions(+), 4 deletions(-) diff --git a/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/mcp-server-creation-enabled.snap b/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/mcp-server-creation-enabled.snap index 7ca32b80a1..9f4d34f54a 100644 --- a/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/mcp-server-creation-enabled.snap +++ b/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/mcp-server-creation-enabled.snap @@ -519,7 +519,7 @@ The Model Context Protocol (MCP) enables communication between the system and MC When a server is connected, you can use the server's tools via the `use_mcp_tool` tool, and access the server's resources via the `access_mcp_resource` tool. - +## test-server (`test-command`) ## Creating an MCP Server The user may ask you something along the lines of "add a tool" that does some function, in other words to create an MCP server that provides tools and resources that may connect to external APIs for example. If they do, you should obtain detailed instructions on this topic using the fetch_instructions tool, like this: diff --git a/src/core/prompts/__tests__/__snapshots__/system-prompt/with-mcp-hub-provided.snap b/src/core/prompts/__tests__/__snapshots__/system-prompt/with-mcp-hub-provided.snap index 7ca32b80a1..9f4d34f54a 100644 --- a/src/core/prompts/__tests__/__snapshots__/system-prompt/with-mcp-hub-provided.snap +++ b/src/core/prompts/__tests__/__snapshots__/system-prompt/with-mcp-hub-provided.snap @@ -519,7 +519,7 @@ The Model Context Protocol (MCP) enables communication between the system and MC When a server is connected, you can use the server's tools via the `use_mcp_tool` tool, and access the server's resources via the `access_mcp_resource` tool. - +## test-server (`test-command`) ## Creating an MCP Server The user may ask you something along the lines of "add a tool" that does some function, in other words to create an MCP server that provides tools and resources that may connect to external APIs for example. If they do, you should obtain detailed instructions on this topic using the fetch_instructions tool, like this: diff --git a/src/core/prompts/__tests__/add-custom-instructions.spec.ts b/src/core/prompts/__tests__/add-custom-instructions.spec.ts index 5097685e3b..c2365925cf 100644 --- a/src/core/prompts/__tests__/add-custom-instructions.spec.ts +++ b/src/core/prompts/__tests__/add-custom-instructions.spec.ts @@ -170,7 +170,36 @@ const mockContext = { // Instead of extending McpHub, create a mock that implements just what we need const createMockMcpHub = (withServers: boolean = false): McpHub => ({ - getServers: () => (withServers ? [{ name: "test-server", disabled: false }] : []), + getServers: () => + withServers + ? [ + { + name: "test-server", + disabled: false, + status: "connected", + config: JSON.stringify({ command: "test-command" }), + tools: [], + resourceTemplates: [], + resources: [], + instructions: undefined, + }, + ] + : [], + getAllServers: () => + withServers + ? [ + { + name: "test-server", + disabled: false, + status: "connected", + config: JSON.stringify({ command: "test-command" }), + tools: [], + resourceTemplates: [], + resources: [], + instructions: undefined, + }, + ] + : [], getMcpServersPath: async () => "/mock/mcp/path", getMcpSettingsFilePath: async () => "/mock/settings/path", dispose: async () => {}, diff --git a/src/core/prompts/__tests__/system-prompt.spec.ts b/src/core/prompts/__tests__/system-prompt.spec.ts index 4d5579408c..a417158e99 100644 --- a/src/core/prompts/__tests__/system-prompt.spec.ts +++ b/src/core/prompts/__tests__/system-prompt.spec.ts @@ -170,7 +170,36 @@ const mockContext = { // Instead of extending McpHub, create a mock that implements just what we need const createMockMcpHub = (withServers: boolean = false): McpHub => ({ - getServers: () => (withServers ? [{ name: "test-server", disabled: false }] : []), + getServers: () => + withServers + ? [ + { + name: "test-server", + disabled: false, + status: "connected", + config: JSON.stringify({ command: "test-command" }), + tools: [], + resourceTemplates: [], + resources: [], + instructions: undefined, + }, + ] + : [], + getAllServers: () => + withServers + ? [ + { + name: "test-server", + disabled: false, + status: "connected", + config: JSON.stringify({ command: "test-command" }), + tools: [], + resourceTemplates: [], + resources: [], + instructions: undefined, + }, + ] + : [], getMcpServersPath: async () => "/mock/mcp/path", getMcpSettingsFilePath: async () => "/mock/settings/path", dispose: async () => {}, From b223d71a7f07a9719879b85c6ae0f9db59a90797 Mon Sep 17 00:00:00 2001 From: Daniel Riccio Date: Wed, 6 Aug 2025 11:06:41 -0500 Subject: [PATCH 4/5] fix: update MCP configuration to use direct object format in YAML - Updated schema to accept { mcp: { included: [...] } } directly as a GroupEntry - Modified McpSelector to create the direct object format instead of tuple - Updated getGroupName and getGroupOptions helpers to handle the new format - Updated MCP server section to properly extract included list from both formats This resolves the nested array issue in YAML generation when toggling MCP checkbox and selecting specific servers. The YAML now correctly generates: - mcp: included: [...] Instead of the previous nested structure. --- packages/types/src/mode.ts | 12 +++- src/core/prompts/sections/mcp-servers.ts | 30 +++++++--- src/shared/modes.ts | 19 ++++++- .../src/components/modes/McpSelector.tsx | 57 +++++++------------ webview-ui/src/components/modes/ModesView.tsx | 8 ++- 5 files changed, 77 insertions(+), 49 deletions(-) diff --git a/packages/types/src/mode.ts b/packages/types/src/mode.ts index 28c5b45927..4a0900d624 100644 --- a/packages/types/src/mode.ts +++ b/packages/types/src/mode.ts @@ -40,7 +40,17 @@ export type GroupOptions = z.infer * GroupEntry */ -export const groupEntrySchema = z.union([toolGroupsSchema, z.tuple([toolGroupsSchema, groupOptionsSchema])]) +export const groupEntrySchema = z.union([ + toolGroupsSchema, + z.tuple([toolGroupsSchema, groupOptionsSchema]), + // Allow direct mcp configuration object + z.object({ + mcp: z.object({ + included: z.array(z.string()), + description: z.string().optional(), + }), + }), +]) export type GroupEntry = z.infer diff --git a/src/core/prompts/sections/mcp-servers.ts b/src/core/prompts/sections/mcp-servers.ts index 62b3a5f6d6..3b2f15e018 100644 --- a/src/core/prompts/sections/mcp-servers.ts +++ b/src/core/prompts/sections/mcp-servers.ts @@ -54,18 +54,34 @@ export async function getMcpServersSection( if (currentMode) { // Find MCP group configuration const mcpGroup = currentMode.groups.find((group: GroupEntry) => { + // Handle tuple format: ["mcp", { mcp: { included: [...] } }] if (Array.isArray(group) && group.length === 2 && group[0] === "mcp") { return true } + // Handle direct object format: { mcp: { included: [...] } } + if (typeof group === "object" && !Array.isArray(group) && "mcp" in group) { + return true + } return getGroupName(group) === "mcp" }) - // If MCP group configuration is found, get mcpIncludedList from mcp.included - if (mcpGroup && Array.isArray(mcpGroup) && mcpGroup.length === 2) { - const options = mcpGroup[1] as { mcp?: { included?: unknown[] } } - mcpIncludedList = Array.isArray(options.mcp?.included) - ? options.mcp.included.filter((item: unknown): item is string => typeof item === "string") - : undefined + // Extract mcpIncludedList based on the format + if (mcpGroup) { + let mcpOptions: { mcp?: { included?: unknown[] } } | undefined + + if (Array.isArray(mcpGroup) && mcpGroup.length === 2) { + // Tuple format + mcpOptions = mcpGroup[1] as { mcp?: { included?: unknown[] } } + } else if (typeof mcpGroup === "object" && !Array.isArray(mcpGroup) && "mcp" in mcpGroup) { + // Direct object format + mcpOptions = mcpGroup as { mcp?: { included?: unknown[] } } + } + + if (mcpOptions) { + mcpIncludedList = Array.isArray(mcpOptions.mcp?.included) + ? mcpOptions.mcp.included.filter((item: unknown): item is string => typeof item === "string") + : undefined + } } } @@ -77,7 +93,7 @@ export async function getMcpServersSection( connectedServers = `${filteredServers .map((server) => { const tools = server.tools - ?.filter((tool) => tool.enabledForPrompt !== false) + ?.filter((tool) => tool.enabledForPrompt !== false) ?.map((tool) => { const schemaStr = tool.inputSchema ? ` Input Schema: diff --git a/src/shared/modes.ts b/src/shared/modes.ts index f68d25c682..f7fb6a5eca 100644 --- a/src/shared/modes.ts +++ b/src/shared/modes.ts @@ -23,13 +23,26 @@ export function getGroupName(group: GroupEntry): ToolGroup { if (typeof group === "string") { return group } - - return group[0] + if (Array.isArray(group)) { + return group[0] + } + // Handle direct MCP object format + if (typeof group === "object" && "mcp" in group) { + return "mcp" as ToolGroup + } + return group as ToolGroup } // Helper to get group options if they exist function getGroupOptions(group: GroupEntry): GroupOptions | undefined { - return Array.isArray(group) ? group[1] : undefined + if (Array.isArray(group)) { + return group[1] + } + // Handle direct MCP object format - return the object itself as options + if (typeof group === "object" && "mcp" in group) { + return group as GroupOptions + } + return undefined } // Helper to check if a file path matches a regex pattern diff --git a/webview-ui/src/components/modes/McpSelector.tsx b/webview-ui/src/components/modes/McpSelector.tsx index 50ce0a1f74..a2a1b57e78 100644 --- a/webview-ui/src/components/modes/McpSelector.tsx +++ b/webview-ui/src/components/modes/McpSelector.tsx @@ -69,45 +69,28 @@ const McpSelector: React.FC = ({ setMcpIncludedList(included) }, [currentMode]) // Handle save - function updateMcpGroupOptions(groups: GroupEntry[] = [], group: string, mcpIncludedList: string[]): GroupEntry[] { - let mcpGroupFound = false - const newGroups = groups - .map((g) => { - if (Array.isArray(g) && g[0] === group) { - mcpGroupFound = true - return [ - group, - { - ...(g[1] || {}), - mcp: mcpIncludedList.length > 0 ? { included: mcpIncludedList } : undefined, - }, - ] as GroupEntry - } - if (typeof g === "string" && g === group) { - mcpGroupFound = true - return [ - group, - { - mcp: mcpIncludedList.length > 0 ? { included: mcpIncludedList } : undefined, - }, - ] as GroupEntry - } - return g - }) - .filter((g) => g !== undefined) + function updateMcpGroupOptions(groups: GroupEntry[] = [], _group: string, mcpIncludedList: string[]): GroupEntry[] { + // Filter out any existing "mcp" entries (both string and object forms) + const filteredGroups = groups.filter((g) => { + if (typeof g === "string") { + return g !== "mcp" + } + if (Array.isArray(g) && g[0] === "mcp") { + return false + } + if (typeof g === "object" && g !== null && !Array.isArray(g) && "mcp" in g) { + return false + } + return true + }) - if (!mcpGroupFound && group === "mcp") { - const groupsWithoutSimpleMcp = newGroups.filter((g) => g !== "mcp") - groupsWithoutSimpleMcp.push([ - "mcp", - { - mcp: mcpIncludedList.length > 0 ? { included: mcpIncludedList } : undefined, - }, - ]) - return groupsWithoutSimpleMcp as GroupEntry[] - } else { - return newGroups as GroupEntry[] + // Add the new MCP configuration if there are selected servers + if (mcpIncludedList.length > 0) { + // Directly add the mcp object without wrapping in an array + return [...filteredGroups, { mcp: { included: mcpIncludedList } }] as GroupEntry[] } + + return filteredGroups as GroupEntry[] } // Handle save diff --git a/webview-ui/src/components/modes/ModesView.tsx b/webview-ui/src/components/modes/ModesView.tsx index f7e4739fe1..1eb2b8bd97 100644 --- a/webview-ui/src/components/modes/ModesView.tsx +++ b/webview-ui/src/components/modes/ModesView.tsx @@ -62,7 +62,13 @@ type ModesViewProps = { // Helper to get group name regardless of format function getGroupName(group: GroupEntry): ToolGroup { - return Array.isArray(group) ? group[0] : group + if (Array.isArray(group)) { + return group[0] + } + if (typeof group === "object" && "mcp" in group) { + return "mcp" as ToolGroup + } + return group as ToolGroup } const ModesView = ({ onDone }: ModesViewProps) => { From 1f4d3c3e7741c6787b06e5482c24cd52d8122477 Mon Sep 17 00:00:00 2001 From: Daniel Riccio Date: Wed, 6 Aug 2025 15:24:59 -0500 Subject: [PATCH 5/5] fix: resolve MCP selector issues and simplify implementation - Fixed useEffect to properly detect MCP object format { mcp: { included: [...] } } - Fixed empty selection behavior to enable all MCP servers instead of disabling MCP - Removed tuple format handling since this feature hasn't been released yet - Simplified code to only handle the new object format for MCP configuration - Removed unused GroupOptions import When no specific servers are selected, MCP is added as a string (enables all servers). When specific servers are selected, MCP is added as an object with the included list. This ensures the MCP selector properly loads configurations and maintains the expected behavior where empty selection means all servers are enabled. --- src/core/prompts/sections/mcp-servers.ts | 30 ++++------------ .../src/components/modes/McpSelector.tsx | 36 ++++++++++--------- 2 files changed, 27 insertions(+), 39 deletions(-) diff --git a/src/core/prompts/sections/mcp-servers.ts b/src/core/prompts/sections/mcp-servers.ts index 3b2f15e018..f3d660f6b4 100644 --- a/src/core/prompts/sections/mcp-servers.ts +++ b/src/core/prompts/sections/mcp-servers.ts @@ -52,36 +52,20 @@ export async function getMcpServersSection( let mcpIncludedList: string[] | undefined if (currentMode) { - // Find MCP group configuration + // Find MCP group configuration - object format: { mcp: { included: [...] } } const mcpGroup = currentMode.groups.find((group: GroupEntry) => { - // Handle tuple format: ["mcp", { mcp: { included: [...] } }] - if (Array.isArray(group) && group.length === 2 && group[0] === "mcp") { - return true - } - // Handle direct object format: { mcp: { included: [...] } } if (typeof group === "object" && !Array.isArray(group) && "mcp" in group) { return true } return getGroupName(group) === "mcp" }) - // Extract mcpIncludedList based on the format - if (mcpGroup) { - let mcpOptions: { mcp?: { included?: unknown[] } } | undefined - - if (Array.isArray(mcpGroup) && mcpGroup.length === 2) { - // Tuple format - mcpOptions = mcpGroup[1] as { mcp?: { included?: unknown[] } } - } else if (typeof mcpGroup === "object" && !Array.isArray(mcpGroup) && "mcp" in mcpGroup) { - // Direct object format - mcpOptions = mcpGroup as { mcp?: { included?: unknown[] } } - } - - if (mcpOptions) { - mcpIncludedList = Array.isArray(mcpOptions.mcp?.included) - ? mcpOptions.mcp.included.filter((item: unknown): item is string => typeof item === "string") - : undefined - } + // Extract mcpIncludedList from the MCP configuration + if (mcpGroup && typeof mcpGroup === "object" && !Array.isArray(mcpGroup) && "mcp" in mcpGroup) { + const mcpOptions = mcpGroup as { mcp?: { included?: unknown[] } } + mcpIncludedList = Array.isArray(mcpOptions.mcp?.included) + ? mcpOptions.mcp.included.filter((item: unknown): item is string => typeof item === "string") + : undefined } } diff --git a/webview-ui/src/components/modes/McpSelector.tsx b/webview-ui/src/components/modes/McpSelector.tsx index a2a1b57e78..eef9459442 100644 --- a/webview-ui/src/components/modes/McpSelector.tsx +++ b/webview-ui/src/components/modes/McpSelector.tsx @@ -14,7 +14,7 @@ import { CommandItem, } from "@src/components/ui" import { useAppTranslation } from "@src/i18n/TranslationContext" -import { ModeConfig, GroupEntry, GroupOptions } from "@roo-code/types" +import { ModeConfig, GroupEntry } from "@roo-code/types" import { McpServer } from "@roo/mcp" interface McpSelectorProps { @@ -55,42 +55,46 @@ const McpSelector: React.FC = ({ return } - const mcpGroupArr = currentMode.groups?.find( - (g: GroupEntry): g is ["mcp", GroupOptions] => Array.isArray(g) && g.length === 2 && g[0] === "mcp", - ) + // Find MCP group - object format: { mcp: { included: [...] } } + const mcpGroup = currentMode.groups?.find((g: GroupEntry) => { + return typeof g === "object" && !Array.isArray(g) && "mcp" in g + }) - const rawGroupOptions: GroupOptions | undefined = mcpGroupArr ? mcpGroupArr[1] : undefined + let included: string[] = [] - const included = Array.isArray(rawGroupOptions?.mcp?.included) - ? (rawGroupOptions.mcp.included.filter((item) => typeof item === "string") as string[]) - : [] + if (mcpGroup && typeof mcpGroup === "object" && !Array.isArray(mcpGroup) && "mcp" in mcpGroup) { + const mcpOptions = mcpGroup as { mcp?: { included?: unknown[] } } + included = Array.isArray(mcpOptions.mcp?.included) + ? (mcpOptions.mcp.included.filter((item) => typeof item === "string") as string[]) + : [] + } // Sync MCP settings when mode changes setMcpIncludedList(included) }, [currentMode]) // Handle save function updateMcpGroupOptions(groups: GroupEntry[] = [], _group: string, mcpIncludedList: string[]): GroupEntry[] { - // Filter out any existing "mcp" entries (both string and object forms) + // Filter out any existing "mcp" entries (string or object forms) const filteredGroups = groups.filter((g) => { if (typeof g === "string") { return g !== "mcp" } - if (Array.isArray(g) && g[0] === "mcp") { - return false - } if (typeof g === "object" && g !== null && !Array.isArray(g) && "mcp" in g) { return false } return true }) - // Add the new MCP configuration if there are selected servers + // Always add MCP back if it's enabled + // If mcpIncludedList is empty, it means all servers are enabled (default behavior) + // If mcpIncludedList has items, only those servers are enabled if (mcpIncludedList.length > 0) { - // Directly add the mcp object without wrapping in an array + // Specific servers selected return [...filteredGroups, { mcp: { included: mcpIncludedList } }] as GroupEntry[] + } else { + // No specific servers selected - enable all (just add "mcp" string) + return [...filteredGroups, "mcp"] as GroupEntry[] } - - return filteredGroups as GroupEntry[] } // Handle save