From 4106ca4a19cf1d3a04dcd09618bbb219aafac078 Mon Sep 17 00:00:00 2001 From: aheizi Date: Thu, 13 Mar 2025 20:55:32 +0800 Subject: [PATCH 1/7] support project-level mcp config --- src/core/webview/ClineProvider.ts | 22 ++++ .../webview/__tests__/ClineProvider.test.ts | 124 ++++++++++++++++++ src/services/mcp/McpHub.ts | 122 ++++++++++++++++- src/shared/WebviewMessage.ts | 1 + src/shared/mcp.ts | 2 + webview-ui/src/components/mcp/McpView.tsx | 34 ++++- 6 files changed, 293 insertions(+), 12 deletions(-) diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index b98934400d2..403c6794df4 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -1114,6 +1114,28 @@ export class ClineProvider implements vscode.WebviewViewProvider { } break } + case "openProjectMcpSettings": { + if (!vscode.workspace.workspaceFolders?.length) { + vscode.window.showErrorMessage("Please open a project folder first") + return + } + + const workspaceFolder = vscode.workspace.workspaceFolders[0] + const rooDir = path.join(workspaceFolder.uri.fsPath, ".roo") + const mcpPath = path.join(rooDir, "mcp.json") + + try { + await fs.mkdir(rooDir, { recursive: true }) + const exists = await fileExistsAtPath(mcpPath) + if (!exists) { + await fs.writeFile(mcpPath, JSON.stringify({ mcpServers: {} }, null, 2)) + } + await openFile(mcpPath) + } catch (error) { + vscode.window.showErrorMessage(`Failed to create or open .roo/mcp.json: ${error}`) + } + break + } case "openCustomModesSettings": { const customModesFilePath = await this.customModesManager.getCustomModesFilePath() if (customModesFilePath) { diff --git a/src/core/webview/__tests__/ClineProvider.test.ts b/src/core/webview/__tests__/ClineProvider.test.ts index abe7a8475af..6d75c01c695 100644 --- a/src/core/webview/__tests__/ClineProvider.test.ts +++ b/src/core/webview/__tests__/ClineProvider.test.ts @@ -1950,6 +1950,130 @@ describe("ClineProvider", () => { }) }) +describe("Project MCP Settings", () => { + let provider: ClineProvider + let mockContext: vscode.ExtensionContext + let mockOutputChannel: vscode.OutputChannel + let mockWebviewView: vscode.WebviewView + let mockPostMessage: jest.Mock + + beforeEach(() => { + jest.clearAllMocks() + + mockContext = { + extensionPath: "/test/path", + extensionUri: {} as vscode.Uri, + globalState: { + get: jest.fn(), + update: jest.fn(), + keys: jest.fn().mockReturnValue([]), + }, + secrets: { + get: jest.fn(), + store: jest.fn(), + delete: jest.fn(), + }, + subscriptions: [], + extension: { + packageJSON: { version: "1.0.0" }, + }, + globalStorageUri: { + fsPath: "/test/storage/path", + }, + } as unknown as vscode.ExtensionContext + + mockOutputChannel = { + appendLine: jest.fn(), + clear: jest.fn(), + dispose: jest.fn(), + } as unknown as vscode.OutputChannel + + mockPostMessage = jest.fn() + mockWebviewView = { + webview: { + postMessage: mockPostMessage, + html: "", + options: {}, + onDidReceiveMessage: jest.fn(), + asWebviewUri: jest.fn(), + }, + visible: true, + onDidDispose: jest.fn(), + onDidChangeVisibility: jest.fn(), + } as unknown as vscode.WebviewView + + provider = new ClineProvider(mockContext, mockOutputChannel) + }) + + test("handles openProjectMcpSettings message", async () => { + await provider.resolveWebviewView(mockWebviewView) + const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0] + + // Mock workspace folders + ;(vscode.workspace as any).workspaceFolders = [{ uri: { fsPath: "/test/workspace" } }] + + // Mock fs functions + const fs = require("fs/promises") + fs.mkdir.mockResolvedValue(undefined) + fs.writeFile.mockResolvedValue(undefined) + + // Trigger openProjectMcpSettings + await messageHandler({ + type: "openProjectMcpSettings", + }) + + // Verify directory was created + expect(fs.mkdir).toHaveBeenCalledWith( + expect.stringContaining(".roo"), + expect.objectContaining({ recursive: true }), + ) + + // Verify file was created with default content + expect(fs.writeFile).toHaveBeenCalledWith( + expect.stringContaining("mcp.json"), + JSON.stringify({ mcpServers: {} }, null, 2), + ) + }) + + test("handles openProjectMcpSettings when workspace is not open", async () => { + await provider.resolveWebviewView(mockWebviewView) + const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0] + + // Mock no workspace folders + ;(vscode.workspace as any).workspaceFolders = [] + + // Trigger openProjectMcpSettings + await messageHandler({ + type: "openProjectMcpSettings", + }) + + // Verify error message was shown + expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("Please open a project folder first") + }) + + test("handles openProjectMcpSettings file creation error", async () => { + await provider.resolveWebviewView(mockWebviewView) + const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0] + + // Mock workspace folders + ;(vscode.workspace as any).workspaceFolders = [{ uri: { fsPath: "/test/workspace" } }] + + // Mock fs functions to fail + const fs = require("fs/promises") + fs.mkdir.mockRejectedValue(new Error("Failed to create directory")) + + // Trigger openProjectMcpSettings + await messageHandler({ + type: "openProjectMcpSettings", + }) + + // Verify error message was shown + expect(vscode.window.showErrorMessage).toHaveBeenCalledWith( + expect.stringContaining("Failed to create or open .roo/mcp.json"), + ) + }) +}) + describe("ContextProxy integration", () => { let provider: ClineProvider let mockContext: vscode.ExtensionContext diff --git a/src/services/mcp/McpHub.ts b/src/services/mcp/McpHub.ts index 7dc1fb85310..bd0ccd5e1ff 100644 --- a/src/services/mcp/McpHub.ts +++ b/src/services/mcp/McpHub.ts @@ -73,6 +73,7 @@ export class McpHub { private providerRef: WeakRef private disposables: vscode.Disposable[] = [] private settingsWatcher?: vscode.FileSystemWatcher + private projectMcpWatcher?: vscode.FileSystemWatcher private fileWatchers: Map = new Map() private isDisposed: boolean = false connections: McpConnection[] = [] @@ -81,9 +82,55 @@ export class McpHub { constructor(provider: ClineProvider) { this.providerRef = new WeakRef(provider) this.watchMcpSettingsFile() + this.watchProjectMcpFile() + this.setupWorkspaceFoldersWatcher() this.initializeMcpServers() } + private setupWorkspaceFoldersWatcher(): void { + this.disposables.push( + vscode.workspace.onDidChangeWorkspaceFolders(async () => { + await this.updateProjectMcpServers() + this.watchProjectMcpFile() + }), + ) + } + + private watchProjectMcpFile(): void { + this.projectMcpWatcher?.dispose() + + this.projectMcpWatcher = vscode.workspace.createFileSystemWatcher("**/.roo/mcp.json", false, false, false) + + this.disposables.push( + this.projectMcpWatcher.onDidChange(async () => { + await this.updateProjectMcpServers() + }), + this.projectMcpWatcher.onDidCreate(async () => { + await this.updateProjectMcpServers() + }), + this.projectMcpWatcher.onDidDelete(async () => { + await this.cleanupProjectMcpServers() + }), + ) + + this.disposables.push(this.projectMcpWatcher) + } + + private async updateProjectMcpServers(): Promise { + await this.cleanupProjectMcpServers() + await this.initializeProjectMcpServers() + } + + private async cleanupProjectMcpServers(): Promise { + const projectServers = this.connections.filter((conn) => conn.server.source === "project") + + for (const conn of projectServers) { + await this.deleteConnection(conn.server.name) + } + + await this.notifyWebviewOfServerChanges() + } + getServers(): McpServer[] { // Only return enabled servers return this.connections.filter((conn) => !conn.server.disabled).map((conn) => conn.server) @@ -158,16 +205,68 @@ export class McpHub { private async initializeMcpServers(): Promise { try { + // 1. Initialize global MCP servers const settingsPath = await this.getMcpSettingsFilePath() const content = await fs.readFile(settingsPath, "utf-8") const config = JSON.parse(content) - await this.updateServerConnections(config.mcpServers || {}) + await this.updateServerConnections(config.mcpServers || {}, "global") + + // 2. Initialize project-level MCP servers + await this.initializeProjectMcpServers() } catch (error) { console.error("Failed to initialize MCP servers:", error) } } - private async connectToServer(name: string, config: z.infer): Promise { + // Get project-level MCP configuration path + private async getProjectMcpPath(): Promise { + if (!vscode.workspace.workspaceFolders?.length) { + return null + } + + const workspaceFolder = vscode.workspace.workspaceFolders[0] + const projectMcpDir = path.join(workspaceFolder.uri.fsPath, ".roo") + const projectMcpPath = path.join(projectMcpDir, "mcp.json") + + try { + await fs.access(projectMcpPath) + return projectMcpPath + } catch { + return null + } + } + + // Initialize project-level MCP servers + private async initializeProjectMcpServers(): Promise { + const projectMcpPath = await this.getProjectMcpPath() + if (!projectMcpPath) { + return + } + + try { + const content = await fs.readFile(projectMcpPath, "utf-8") + const config = JSON.parse(content) + + // Validate configuration structure + const result = McpSettingsSchema.safeParse(config) + if (!result.success) { + vscode.window.showErrorMessage("项目 MCP 配置格式无效") + return + } + + // Update server connections + await this.updateServerConnections(result.data.mcpServers || {}, "project") + } catch (error) { + console.error("Failed to initialize project MCP servers:", error) + vscode.window.showErrorMessage(`初始化项目 MCP 服务器失败: ${error}`) + } + } + + private async connectToServer( + name: string, + config: z.infer, + source: "global" | "project" = "global", + ): Promise { // Remove existing connection if it exists await this.deleteConnection(name) @@ -272,6 +371,8 @@ export class McpHub { config: JSON.stringify(config), status: "connecting", disabled: config.disabled, + source, + projectPath: source === "project" ? vscode.workspace.workspaceFolders?.[0]?.uri.fsPath : undefined, }, client, transport, @@ -366,10 +467,17 @@ export class McpHub { } } - async updateServerConnections(newServers: Record): Promise { + async updateServerConnections( + newServers: Record, + source: "global" | "project" = "global", + ): Promise { this.isConnecting = true this.removeAllFileWatchers() - const currentNames = new Set(this.connections.map((conn) => conn.server.name)) + // Filter connections by source + const currentConnections = this.connections.filter( + (conn) => conn.server.source === source || (!conn.server.source && source === "global"), + ) + const currentNames = new Set(currentConnections.map((conn) => conn.server.name)) const newNames = new Set(Object.keys(newServers)) // Delete removed servers @@ -388,7 +496,7 @@ export class McpHub { // New server try { this.setupFileWatcher(name, config) - await this.connectToServer(name, config) + await this.connectToServer(name, config, source) } catch (error) { console.error(`Failed to connect to new MCP server ${name}:`, error) } @@ -397,8 +505,8 @@ export class McpHub { try { this.setupFileWatcher(name, config) await this.deleteConnection(name) - await this.connectToServer(name, config) - console.log(`Reconnected MCP server with updated config: ${name}`) + await this.connectToServer(name, config, source) + console.log(`Reconnected ${source} MCP server with updated config: ${name}`) } catch (error) { console.error(`Failed to reconnect MCP server ${name}:`, error) } diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts index e9a64d891b3..f00a9edd113 100644 --- a/src/shared/WebviewMessage.ts +++ b/src/shared/WebviewMessage.ts @@ -59,6 +59,7 @@ export interface WebviewMessage { | "screenshotQuality" | "remoteBrowserHost" | "openMcpSettings" + | "openProjectMcpSettings" | "restartMcpServer" | "toggleToolAlwaysAllow" | "toggleMcpServer" diff --git a/src/shared/mcp.ts b/src/shared/mcp.ts index 2bc38a12a8e..7a490851bcf 100644 --- a/src/shared/mcp.ts +++ b/src/shared/mcp.ts @@ -8,6 +8,8 @@ export type McpServer = { resourceTemplates?: McpResourceTemplate[] disabled?: boolean timeout?: number + source?: "global" | "project" + projectPath?: string } export type McpTool = { diff --git a/webview-ui/src/components/mcp/McpView.tsx b/webview-ui/src/components/mcp/McpView.tsx index ce37a4c09d8..b615f96d085 100644 --- a/webview-ui/src/components/mcp/McpView.tsx +++ b/webview-ui/src/components/mcp/McpView.tsx @@ -94,16 +94,25 @@ const McpView = ({ onDone }: McpViewProps) => { )} - {/* Edit Settings Button */} -
+ {/* Edit Settings Buttons */} +
{ vscode.postMessage({ type: "openMcpSettings" }) }}> - Edit MCP Settings + Edit Global MCP + + { + vscode.postMessage({ type: "openProjectMcpSettings" }) + }}> + + Edit Project MCP
@@ -193,7 +202,22 @@ const ServerRow = ({ server, alwaysAllowMcp }: { server: McpServer; alwaysAllowM style={{ marginRight: "8px" }} /> )} - {server.name} + + {server.name} + {server.source && ( + + {server.source} + + )} +
e.stopPropagation()}> From f77606996f6a1a6ab420bbce7507f4993d772872 Mon Sep 17 00:00:00 2001 From: aheizi Date: Fri, 14 Mar 2025 20:59:21 +0800 Subject: [PATCH 2/7] Merge branch 'main' into support_project_mcp --- src/services/mcp/McpHub.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/mcp/McpHub.ts b/src/services/mcp/McpHub.ts index 4c904d0d245..46becd97a74 100644 --- a/src/services/mcp/McpHub.ts +++ b/src/services/mcp/McpHub.ts @@ -244,7 +244,7 @@ export class McpHub { private async connectToServer( name: string, - config: z.infer, + config: z.infer, source: "global" | "project" = "global", ): Promise { // Remove existing connection if it exists (should never happen, the connection should be deleted beforehand) From 26941dcaae32554a9b83d5935d05ca895d0006ed Mon Sep 17 00:00:00 2001 From: aheizi Date: Sat, 15 Mar 2025 17:33:04 +0800 Subject: [PATCH 3/7] switch the toasts to English fix test --- src/services/mcp/McpHub.ts | 10 +++++++--- src/services/mcp/__tests__/McpHub.test.ts | 22 +++++++++++++++++++++- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/src/services/mcp/McpHub.ts b/src/services/mcp/McpHub.ts index 46becd97a74..488e77b80a6 100644 --- a/src/services/mcp/McpHub.ts +++ b/src/services/mcp/McpHub.ts @@ -67,7 +67,11 @@ export class McpHub { this.initializeMcpServers() } - private setupWorkspaceFoldersWatcher(): void { + public setupWorkspaceFoldersWatcher(): void { + // Skip if test environment is detected + if (process.env.NODE_ENV === "test" || process.env.JEST_WORKER_ID !== undefined) { + return + } this.disposables.push( vscode.workspace.onDidChangeWorkspaceFolders(async () => { await this.updateProjectMcpServers() @@ -230,7 +234,7 @@ export class McpHub { // Validate configuration structure const result = McpSettingsSchema.safeParse(config) if (!result.success) { - vscode.window.showErrorMessage("项目 MCP 配置格式无效") + vscode.window.showErrorMessage("Invalid project MCP configuration format") return } @@ -238,7 +242,7 @@ export class McpHub { await this.updateServerConnections(result.data.mcpServers || {}, "project") } catch (error) { console.error("Failed to initialize project MCP servers:", error) - vscode.window.showErrorMessage(`初始化项目 MCP 服务器失败: ${error}`) + vscode.window.showErrorMessage(`Failed to initialize project MCP server: ${error}`) } } diff --git a/src/services/mcp/__tests__/McpHub.test.ts b/src/services/mcp/__tests__/McpHub.test.ts index b418bae21a7..cd0f6e3eb2a 100644 --- a/src/services/mcp/__tests__/McpHub.test.ts +++ b/src/services/mcp/__tests__/McpHub.test.ts @@ -7,7 +7,27 @@ import { StdioConfigSchema } from "../McpHub" const fs = require("fs/promises") const { McpHub } = require("../McpHub") -jest.mock("vscode") +jest.mock("vscode", () => ({ + workspace: { + createFileSystemWatcher: jest.fn().mockReturnValue({ + onDidChange: jest.fn(), + onDidCreate: jest.fn(), + onDidDelete: jest.fn(), + dispose: jest.fn(), + }), + onDidSaveTextDocument: jest.fn(), + onDidChangeWorkspaceFolders: jest.fn(), + workspaceFolders: [], + }, + window: { + showErrorMessage: jest.fn(), + showInformationMessage: jest.fn(), + showWarningMessage: jest.fn(), + }, + Disposable: { + from: jest.fn(), + }, +})) jest.mock("fs/promises") jest.mock("../../../core/webview/ClineProvider") From 792a8225c15a01ec5dc8cab660c5a2bfa8944b3c Mon Sep 17 00:00:00 2001 From: aheizi Date: Mon, 17 Mar 2025 11:14:33 +0800 Subject: [PATCH 4/7] add i18n for project mcp --- webview-ui/src/components/mcp/McpView.tsx | 4 ++-- webview-ui/src/i18n/locales/ar/mcp.json | 3 ++- webview-ui/src/i18n/locales/ca/mcp.json | 3 ++- webview-ui/src/i18n/locales/cs/mcp.json | 3 ++- webview-ui/src/i18n/locales/de/mcp.json | 3 ++- webview-ui/src/i18n/locales/en/mcp.json | 3 ++- webview-ui/src/i18n/locales/es/mcp.json | 3 ++- webview-ui/src/i18n/locales/fr/mcp.json | 3 ++- webview-ui/src/i18n/locales/hi/mcp.json | 3 ++- webview-ui/src/i18n/locales/hu/mcp.json | 3 ++- webview-ui/src/i18n/locales/it/mcp.json | 3 ++- webview-ui/src/i18n/locales/ja/mcp.json | 3 ++- webview-ui/src/i18n/locales/ko/mcp.json | 3 ++- webview-ui/src/i18n/locales/pl/mcp.json | 3 ++- webview-ui/src/i18n/locales/pt-BR/mcp.json | 3 ++- webview-ui/src/i18n/locales/pt/mcp.json | 3 ++- webview-ui/src/i18n/locales/ru/mcp.json | 3 ++- webview-ui/src/i18n/locales/tr/mcp.json | 3 ++- webview-ui/src/i18n/locales/zh-CN/mcp.json | 2 ++ webview-ui/src/i18n/locales/zh-TW/mcp.json | 2 ++ 20 files changed, 40 insertions(+), 19 deletions(-) diff --git a/webview-ui/src/components/mcp/McpView.tsx b/webview-ui/src/components/mcp/McpView.tsx index ddfbeca2df2..3295e7b2c01 100644 --- a/webview-ui/src/components/mcp/McpView.tsx +++ b/webview-ui/src/components/mcp/McpView.tsx @@ -103,7 +103,7 @@ const McpView = ({ onDone }: McpViewProps) => { vscode.postMessage({ type: "openMcpSettings" }) }}> - Edit Global MCP + {t("mcp:editGlobalMCP")} { vscode.postMessage({ type: "openProjectMcpSettings" }) }}> - {t("mcp:editSettings")} + {t("mcp:editProjectMCP")}
diff --git a/webview-ui/src/i18n/locales/ar/mcp.json b/webview-ui/src/i18n/locales/ar/mcp.json index e3413232337..dddd9ef3b12 100644 --- a/webview-ui/src/i18n/locales/ar/mcp.json +++ b/webview-ui/src/i18n/locales/ar/mcp.json @@ -10,7 +10,8 @@ "title": "تمكين إنشاء خادم MCP", "description": "عند التمكين، يمكن لـ Roo مساعدتك في إنشاء خوادم MCP جديدة عبر أوامر مثل \"إضافة أداة جديدة إلى...\". إذا كنت لا تحتاج إلى إنشاء خوادم MCP، يمكنك تعطيل هذا لتقليل استخدام token بواسطة Roo." }, - "editSettings": "تعديل إعدادات MCP", + "editGlobalMCP": "تعديل MCP العام", + "editProjectMCP": "تعديل MCP المشروع", "tool": { "alwaysAllow": "السماح دائمًا", "parameters": "المعلمات", diff --git a/webview-ui/src/i18n/locales/ca/mcp.json b/webview-ui/src/i18n/locales/ca/mcp.json index 1f339dd09f5..8da2cbb953c 100644 --- a/webview-ui/src/i18n/locales/ca/mcp.json +++ b/webview-ui/src/i18n/locales/ca/mcp.json @@ -10,7 +10,8 @@ "title": "Habilitar creació de servidors MCP", "description": "Quan està habilitat, Roo pot ajudar-te a crear nous servidors MCP mitjançant ordres com \"afegir una nova eina per a...\". Si no necessites crear servidors MCP, pots desactivar això per reduir l'ús de tokens de Roo." }, - "editSettings": "Editar configuració de MCP", + "editGlobalMCP": "Editar MCP Global", + "editProjectMCP": "Editar MCP del Projecte", "tool": { "alwaysAllow": "Permetre sempre", "parameters": "Paràmetres", diff --git a/webview-ui/src/i18n/locales/cs/mcp.json b/webview-ui/src/i18n/locales/cs/mcp.json index 2eca6d2e10b..7afbc6be472 100644 --- a/webview-ui/src/i18n/locales/cs/mcp.json +++ b/webview-ui/src/i18n/locales/cs/mcp.json @@ -10,7 +10,8 @@ "title": "Povolit vytváření MCP serverů", "description": "Když je povoleno, Roo vám může pomoci vytvářet nové MCP servery pomocí příkazů jako \"přidat nový nástroj pro...\". Pokud nepotřebujete vytvářet MCP servery, můžete tuto funkci zakázat a snížit spotřebu tokenů Roo." }, - "editSettings": "Upravit nastavení MCP", + "editGlobalMCP": "Upravit globální MCP", + "editProjectMCP": "Upravit projektové MCP", "tool": { "alwaysAllow": "Vždy povolit", "parameters": "Parametry", diff --git a/webview-ui/src/i18n/locales/de/mcp.json b/webview-ui/src/i18n/locales/de/mcp.json index c7d11df0513..1da18544f3c 100644 --- a/webview-ui/src/i18n/locales/de/mcp.json +++ b/webview-ui/src/i18n/locales/de/mcp.json @@ -10,7 +10,8 @@ "title": "MCP-Server-Erstellung aktivieren", "description": "Wenn aktiviert, kann Roo Ihnen helfen, neue MCP-Server über Befehle wie \"neues Tool hinzufügen zu...\" zu erstellen. Wenn Sie keine MCP-Server erstellen müssen, können Sie dies deaktivieren, um den Token-Verbrauch von Roo zu reduzieren." }, - "editSettings": "MCP-Einstellungen bearbeiten", + "editGlobalMCP": "Globales MCP bearbeiten", + "editProjectMCP": "Projekt-MCP bearbeiten", "tool": { "alwaysAllow": "Immer erlauben", "parameters": "Parameter", diff --git a/webview-ui/src/i18n/locales/en/mcp.json b/webview-ui/src/i18n/locales/en/mcp.json index 710b787e5d4..95ec55bd0c5 100644 --- a/webview-ui/src/i18n/locales/en/mcp.json +++ b/webview-ui/src/i18n/locales/en/mcp.json @@ -10,7 +10,8 @@ "title": "Enable MCP Server Creation", "description": "When enabled, Roo can help you create new MCP servers via commands like \"add a new tool to...\". If you don't need to create MCP servers you can disable this to reduce Roo's token usage." }, - "editSettings": "Edit MCP Settings", + "editGlobalMCP": "Edit Global MCP", + "editProjectMCP": "Edit Project MCP", "tool": { "alwaysAllow": "Always allow", "parameters": "Parameters", diff --git a/webview-ui/src/i18n/locales/es/mcp.json b/webview-ui/src/i18n/locales/es/mcp.json index 8cc199ad254..6b6c7eb1988 100644 --- a/webview-ui/src/i18n/locales/es/mcp.json +++ b/webview-ui/src/i18n/locales/es/mcp.json @@ -10,7 +10,8 @@ "title": "Habilitar creación de servidores MCP", "description": "Cuando está habilitado, Roo puede ayudarte a crear nuevos servidores MCP mediante comandos como \"añadir una nueva herramienta para...\". Si no necesitas crear servidores MCP, puedes desactivar esto para reducir el uso de tokens de Roo." }, - "editSettings": "Editar configuración de MCP", + "editGlobalMCP": "Editar MCP Global", + "editProjectMCP": "Editar MCP del Proyecto", "tool": { "alwaysAllow": "Permitir siempre", "parameters": "Parámetros", diff --git a/webview-ui/src/i18n/locales/fr/mcp.json b/webview-ui/src/i18n/locales/fr/mcp.json index 2a239cd33e1..2f3195067ca 100644 --- a/webview-ui/src/i18n/locales/fr/mcp.json +++ b/webview-ui/src/i18n/locales/fr/mcp.json @@ -10,7 +10,8 @@ "title": "Activer la création de serveurs MCP", "description": "Lorsqu'activé, Roo peut vous aider à créer de nouveaux serveurs MCP via des commandes comme \"ajouter un nouvel outil pour...\". Si vous n'avez pas besoin de créer des serveurs MCP, vous pouvez désactiver cette option pour réduire l'utilisation de tokens par Roo." }, - "editSettings": "Modifier les paramètres MCP", + "editGlobalMCP": "Modifier MCP Global", + "editProjectMCP": "Modifier MCP du Projet", "tool": { "alwaysAllow": "Toujours autoriser", "parameters": "Paramètres", diff --git a/webview-ui/src/i18n/locales/hi/mcp.json b/webview-ui/src/i18n/locales/hi/mcp.json index 5827787b67d..32aee49dc49 100644 --- a/webview-ui/src/i18n/locales/hi/mcp.json +++ b/webview-ui/src/i18n/locales/hi/mcp.json @@ -10,7 +10,8 @@ "title": "MCP सर्वर निर्माण सक्षम करें", "description": "जब सक्षम होता है, तो Roo आपको \"में नया उपकरण जोड़ें...\" जैसे कमांड के माध्यम से नए MCP सर्वर बनाने में मदद कर सकता है। यदि आपको MCP सर्वर बनाने की आवश्यकता नहीं है, तो आप Roo के token उपयोग को कम करने के लिए इसे अक्षम कर सकते हैं।" }, - "editSettings": "MCP सेटिंग्स संपादित करें", + "editGlobalMCP": "वैश्विक MCP संपादित करें", + "editProjectMCP": "प्रोजेक्ट MCP संपादित करें", "tool": { "alwaysAllow": "हमेशा अनुमति दें", "parameters": "पैरामीटर", diff --git a/webview-ui/src/i18n/locales/hu/mcp.json b/webview-ui/src/i18n/locales/hu/mcp.json index 8500b16d966..7c5de063f69 100644 --- a/webview-ui/src/i18n/locales/hu/mcp.json +++ b/webview-ui/src/i18n/locales/hu/mcp.json @@ -10,7 +10,8 @@ "title": "MCP szerver létrehozás engedélyezése", "description": "Ha engedélyezve van, a Roo segíthet új MCP szerverek létrehozásában olyan parancsokkal, mint \"új eszköz hozzáadása...\". Ha nincs szükséged MCP szerverek létrehozására, kikapcsolhatod ezt, hogy csökkentsd a Roo token használatát." }, - "editSettings": "MCP beállítások szerkesztése", + "editGlobalMCP": "Globális MCP szerkesztése", + "editProjectMCP": "Projekt MCP szerkesztése", "tool": { "alwaysAllow": "Mindig engedélyez", "parameters": "Paraméterek", diff --git a/webview-ui/src/i18n/locales/it/mcp.json b/webview-ui/src/i18n/locales/it/mcp.json index c297cca91aa..18bc9b487ec 100644 --- a/webview-ui/src/i18n/locales/it/mcp.json +++ b/webview-ui/src/i18n/locales/it/mcp.json @@ -10,7 +10,8 @@ "title": "Abilita creazione server MCP", "description": "Quando abilitato, Roo può aiutarti a creare nuovi server MCP tramite comandi come \"aggiungi un nuovo strumento per...\". Se non hai bisogno di creare server MCP, puoi disabilitare questa opzione per ridurre l'utilizzo di token da parte di Roo." }, - "editSettings": "Modifica impostazioni MCP", + "editGlobalMCP": "Modifica MCP Globale", + "editProjectMCP": "Modifica MCP del Progetto", "tool": { "alwaysAllow": "Consenti sempre", "parameters": "Parametri", diff --git a/webview-ui/src/i18n/locales/ja/mcp.json b/webview-ui/src/i18n/locales/ja/mcp.json index 62e340db843..d5920c1c46f 100644 --- a/webview-ui/src/i18n/locales/ja/mcp.json +++ b/webview-ui/src/i18n/locales/ja/mcp.json @@ -10,7 +10,8 @@ "title": "MCPサーバー作成を有効にする", "description": "有効にすると、Rooは「新しいツールを追加する...」などのコマンドを通じて新しいMCPサーバーの作成を支援できます。MCPサーバーを作成する必要がない場合は、これを無効にしてRooのtoken使用量を減らすことができます。" }, - "editSettings": "MCP設定を編集", + "editGlobalMCP": "グローバルMCPを編集", + "editProjectMCP": "プロジェクトMCPを編集", "tool": { "alwaysAllow": "常に許可", "parameters": "パラメータ", diff --git a/webview-ui/src/i18n/locales/ko/mcp.json b/webview-ui/src/i18n/locales/ko/mcp.json index f79409d441d..5d45eb0fca2 100644 --- a/webview-ui/src/i18n/locales/ko/mcp.json +++ b/webview-ui/src/i18n/locales/ko/mcp.json @@ -10,7 +10,8 @@ "title": "MCP 서버 생성 활성화", "description": "활성화하면 Roo가 \"새 도구 추가...\"와 같은 명령을 통해 새 MCP 서버를 만드는 데 도움을 줄 수 있습니다. MCP 서버를 만들 필요가 없다면 이 기능을 비활성화하여 Roo의 token 사용량을 줄일 수 있습니다." }, - "editSettings": "MCP 설정 편집", + "editGlobalMCP": "전역 MCP 편집", + "editProjectMCP": "프로젝트 MCP 편집", "tool": { "alwaysAllow": "항상 허용", "parameters": "매개변수", diff --git a/webview-ui/src/i18n/locales/pl/mcp.json b/webview-ui/src/i18n/locales/pl/mcp.json index 7308698d795..4f1f2a9411b 100644 --- a/webview-ui/src/i18n/locales/pl/mcp.json +++ b/webview-ui/src/i18n/locales/pl/mcp.json @@ -10,7 +10,8 @@ "title": "Włącz tworzenie serwerów MCP", "description": "Po włączeniu, Roo może pomóc w tworzeniu nowych serwerów MCP za pomocą poleceń takich jak \"dodaj nowe narzędzie do...\". Jeśli nie potrzebujesz tworzyć serwerów MCP, możesz to wyłączyć, aby zmniejszyć zużycie tokenów przez Roo." }, - "editSettings": "Edytuj ustawienia MCP", + "editGlobalMCP": "Edytuj globalne MCP", + "editProjectMCP": "Edytuj projektowe MCP", "tool": { "alwaysAllow": "Zawsze zezwalaj", "parameters": "Parametry", diff --git a/webview-ui/src/i18n/locales/pt-BR/mcp.json b/webview-ui/src/i18n/locales/pt-BR/mcp.json index e5713608ebb..2c8c282e0d4 100644 --- a/webview-ui/src/i18n/locales/pt-BR/mcp.json +++ b/webview-ui/src/i18n/locales/pt-BR/mcp.json @@ -10,7 +10,8 @@ "title": "Ativar criação de servidores MCP", "description": "Quando ativado, o Roo pode ajudar você a criar novos servidores MCP por meio de comandos como \"adicionar uma nova ferramenta para...\". Se você não precisar criar servidores MCP, pode desativar isso para reduzir o uso de tokens do Roo." }, - "editSettings": "Editar configurações do MCP", + "editGlobalMCP": "Editar MCP Global", + "editProjectMCP": "Editar MCP do Projeto", "tool": { "alwaysAllow": "Sempre permitir", "parameters": "Parâmetros", diff --git a/webview-ui/src/i18n/locales/pt/mcp.json b/webview-ui/src/i18n/locales/pt/mcp.json index e5713608ebb..2c8c282e0d4 100644 --- a/webview-ui/src/i18n/locales/pt/mcp.json +++ b/webview-ui/src/i18n/locales/pt/mcp.json @@ -10,7 +10,8 @@ "title": "Ativar criação de servidores MCP", "description": "Quando ativado, o Roo pode ajudar você a criar novos servidores MCP por meio de comandos como \"adicionar uma nova ferramenta para...\". Se você não precisar criar servidores MCP, pode desativar isso para reduzir o uso de tokens do Roo." }, - "editSettings": "Editar configurações do MCP", + "editGlobalMCP": "Editar MCP Global", + "editProjectMCP": "Editar MCP do Projeto", "tool": { "alwaysAllow": "Sempre permitir", "parameters": "Parâmetros", diff --git a/webview-ui/src/i18n/locales/ru/mcp.json b/webview-ui/src/i18n/locales/ru/mcp.json index 9a81487bc52..4236933c313 100644 --- a/webview-ui/src/i18n/locales/ru/mcp.json +++ b/webview-ui/src/i18n/locales/ru/mcp.json @@ -10,7 +10,8 @@ "title": "Включить создание серверов MCP", "description": "Когда включено, Roo может помочь вам создать новые серверы MCP с помощью команд типа \"добавить новый инструмент для...\". Если вам не нужно создавать серверы MCP, вы можете отключить это, чтобы снизить использование token Roo." }, - "editSettings": "Редактировать настройки MCP", + "editGlobalMCP": "Редактировать глобальный MCP", + "editProjectMCP": "Редактировать проект MCP", "tool": { "alwaysAllow": "Всегда разрешать", "parameters": "Параметры", diff --git a/webview-ui/src/i18n/locales/tr/mcp.json b/webview-ui/src/i18n/locales/tr/mcp.json index f0610630c0e..c21b009773c 100644 --- a/webview-ui/src/i18n/locales/tr/mcp.json +++ b/webview-ui/src/i18n/locales/tr/mcp.json @@ -10,7 +10,8 @@ "title": "MCP Sunucu Oluşturmayı Etkinleştir", "description": "Etkinleştirildiğinde, Roo \"için yeni bir araç ekle...\" gibi komutlar aracılığıyla yeni MCP sunucuları oluşturmanıza yardımcı olabilir. MCP sunucuları oluşturmanız gerekmiyorsa, Roo'nun token kullanımını azaltmak için bunu devre dışı bırakabilirsiniz." }, - "editSettings": "MCP Ayarlarını Düzenle", + "editGlobalMCP": "Global MCP'yi Düzenle", + "editProjectMCP": "Proje MCP'yi Düzenle", "tool": { "alwaysAllow": "Her zaman izin ver", "parameters": "Parametreler", diff --git a/webview-ui/src/i18n/locales/zh-CN/mcp.json b/webview-ui/src/i18n/locales/zh-CN/mcp.json index f57f4190915..703ae431404 100644 --- a/webview-ui/src/i18n/locales/zh-CN/mcp.json +++ b/webview-ui/src/i18n/locales/zh-CN/mcp.json @@ -10,6 +10,8 @@ "title": "启用 MCP 服务器创建", "description": "启用后,Roo 可以通过诸如\"添加新工具到...\"之类的命令帮助您创建新的 MCP 服务器。如果您不需要创建 MCP 服务器,可以禁用此功能以减少 Roo 的 token 使用量。" }, + "editGlobalMCP": "编辑全局 MCP", + "editProjectMCP": "编辑项目 MCP", "editSettings": "编辑 MCP 设置", "tool": { "alwaysAllow": "始终允许", diff --git a/webview-ui/src/i18n/locales/zh-TW/mcp.json b/webview-ui/src/i18n/locales/zh-TW/mcp.json index e8ddba2f2c1..ee856611013 100644 --- a/webview-ui/src/i18n/locales/zh-TW/mcp.json +++ b/webview-ui/src/i18n/locales/zh-TW/mcp.json @@ -10,6 +10,8 @@ "title": "啟用 MCP 伺服器創建", "description": "啟用後,Roo 可以通過如\"新增工具到...\"之類的命令幫助您創建新的 MCP 伺服器。如果您不需要創建 MCP 伺服器,可以停用此功能以減少 Roo 的 token 使用量。" }, + "editGlobalMCP": "編輯全域 MCP", + "editProjectMCP": "編輯專案 MCP", "editSettings": "編輯 MCP 設定", "tool": { "alwaysAllow": "始終允許", From c201baa4d13bbcc1d7ebce60f1e5afb0042d20fe Mon Sep 17 00:00:00 2001 From: aheizi Date: Mon, 17 Mar 2025 12:05:23 +0800 Subject: [PATCH 5/7] fix mcp connection --- src/services/mcp/McpHub.ts | 64 ++++++++++++++++++++++++++++---------- 1 file changed, 48 insertions(+), 16 deletions(-) diff --git a/src/services/mcp/McpHub.ts b/src/services/mcp/McpHub.ts index 488e77b80a6..4248649d5cf 100644 --- a/src/services/mcp/McpHub.ts +++ b/src/services/mcp/McpHub.ts @@ -338,11 +338,20 @@ export class McpHub { console.error(`Server "${name}" stderr:`, errorOutput) const connection = this.connections.find((conn) => conn.server.name === name) if (connection) { - // NOTE: we do not set server status to "disconnected" because stderr logs do not necessarily mean the server crashed or disconnected, it could just be informational. In fact when the server first starts up, it immediately logs " server running on stdio" to stderr. - this.appendErrorMessage(connection, errorOutput) - // Only need to update webview right away if it's already disconnected - if (connection.server.status === "disconnected") { - await this.notifyWebviewOfServerChanges() + // Filter out normal startup messages that appear in stderr + const isStartupMessage = + errorOutput.includes("server running") || + errorOutput.includes("MCP server running") || + errorOutput.includes('mode: "stdio"') + + // Only append to error message if it's not a startup message + if (!isStartupMessage) { + // NOTE: we do not set server status to "disconnected" because stderr logs do not necessarily mean the server crashed or disconnected, it could just be informational. + this.appendErrorMessage(connection, errorOutput) + // Only need to update webview right away if it's already disconnected + if (connection.server.status === "disconnected") { + await this.notifyWebviewOfServerChanges() + } } } }) @@ -526,13 +535,15 @@ export class McpHub { if (config) { vscode.window.showInformationMessage(`Restarting ${serverName} MCP server...`) connection.server.status = "connecting" - connection.server.error = "" + connection.server.error = "" // Clear any previous error messages await this.notifyWebviewOfServerChanges() await delay(500) // artificial delay to show user that server is restarting try { + // Save the original source before deleting the connection + const source = connection.server.source || "global" await this.deleteConnection(serverName) - // Try to connect again using existing config - await this.connectToServer(serverName, JSON.parse(config)) + // Try to connect again using existing config and preserve the original source + await this.connectToServer(serverName, JSON.parse(config), source) vscode.window.showInformationMessage(`${serverName} MCP server connected`) } catch (error) { console.error(`Failed to restart connection for ${serverName}:`, error) @@ -692,16 +703,30 @@ export class McpHub { public async deleteServer(serverName: string): Promise { try { - const settingsPath = await this.getMcpSettingsFilePath() + // Find the connection to determine if it's a global or project server + const connection = this.connections.find((conn) => conn.server.name === serverName) + const isProjectServer = connection?.server.source === "project" + + // Determine which config file to modify + let configPath: string + if (isProjectServer) { + const projectMcpPath = await this.getProjectMcpPath() + if (!projectMcpPath) { + throw new Error("Project MCP configuration file not found") + } + configPath = projectMcpPath + } else { + configPath = await this.getMcpSettingsFilePath() + } - // Ensure the settings file exists and is accessible + // Ensure the config file exists and is accessible try { - await fs.access(settingsPath) + await fs.access(configPath) } catch (error) { - throw new Error("Settings file not accessible") + throw new Error(`Configuration file not accessible: ${configPath}`) } - const content = await fs.readFile(settingsPath, "utf-8") + const content = await fs.readFile(configPath, "utf-8") const config = JSON.parse(content) // Validate the config structure @@ -722,10 +747,17 @@ export class McpHub { mcpServers: config.mcpServers, } - await fs.writeFile(settingsPath, JSON.stringify(updatedConfig, null, 2)) + await fs.writeFile(configPath, JSON.stringify(updatedConfig, null, 2)) - // Update server connections - await this.updateServerConnections(config.mcpServers) + // Delete the connection + await this.deleteConnection(serverName) + + // If it's a project server, update project servers, otherwise update global servers + if (isProjectServer) { + await this.updateProjectMcpServers() + } else { + await this.updateServerConnections(config.mcpServers) + } vscode.window.showInformationMessage(`Deleted MCP server: ${serverName}`) } else { From 8f4145efd28678146b006da876a51b21786ee903 Mon Sep 17 00:00:00 2001 From: aheizi Date: Mon, 17 Mar 2025 23:57:59 +0800 Subject: [PATCH 6/7] optimize McpHub.ts --- src/services/mcp/McpHub.ts | 99 +++++++++++++++++++++++++++----------- 1 file changed, 71 insertions(+), 28 deletions(-) diff --git a/src/services/mcp/McpHub.ts b/src/services/mcp/McpHub.ts index dc5389ffaf4..d2f588fe643 100644 --- a/src/services/mcp/McpHub.ts +++ b/src/services/mcp/McpHub.ts @@ -112,7 +112,8 @@ export class McpHub { this.watchMcpSettingsFile() this.watchProjectMcpFile() this.setupWorkspaceFoldersWatcher() - this.initializeMcpServers() + this.initializeGlobalMcpServers() + this.initializeProjectMcpServers() } public setupWorkspaceFoldersWatcher(): void { @@ -149,17 +150,20 @@ export class McpHub { } private async updateProjectMcpServers(): Promise { + // Only clean up and initialize project servers, not affecting global servers await this.cleanupProjectMcpServers() await this.initializeProjectMcpServers() } private async cleanupProjectMcpServers(): Promise { + // Only filter and delete project servers const projectServers = this.connections.filter((conn) => conn.server.source === "project") for (const conn of projectServers) { await this.deleteConnection(conn.server.name) } + // Notify webview of changes after cleanup await this.notifyWebviewOfServerChanges() } @@ -299,7 +303,8 @@ export class McpHub { return } try { - await this.updateServerConnections(result.data.mcpServers || {}) + // Only update global servers when global settings change + await this.updateServerConnections(result.data.mcpServers || {}, "global") } catch (error) { this.showErrorMessage("Failed to process MCP settings change", error) } @@ -308,9 +313,9 @@ export class McpHub { ) } - private async initializeMcpServers(): Promise { + private async initializeGlobalMcpServers(): Promise { try { - // 1. Initialize global MCP servers + // Initialize global MCP servers const settingsPath = await this.getMcpSettingsFilePath() const content = await fs.readFile(settingsPath, "utf-8") let config: any @@ -340,15 +345,12 @@ export class McpHub { // Still try to connect with the raw config, but show warnings try { await this.updateServerConnections(config.mcpServers || {}, "global") - - // 2. Initialize project-level MCP servers - await this.initializeProjectMcpServers() } catch (error) { - this.showErrorMessage("Failed to initialize MCP servers with raw config", error) + this.showErrorMessage("Failed to initialize global MCP servers with raw config", error) } } } catch (error) { - this.showErrorMessage("Failed to initialize MCP servers", error) + this.showErrorMessage("Failed to initialize global MCP servers", error) } } @@ -454,15 +456,22 @@ export class McpHub { const stderrStream = transport.stderr if (stderrStream) { stderrStream.on("data", async (data: Buffer) => { - const errorOutput = data.toString() - console.error(`Server "${name}" stderr:`, errorOutput) - const connection = this.connections.find((conn) => conn.server.name === name) - if (connection) { - // NOTE: we do not set server status to "disconnected" because stderr logs do not necessarily mean the server crashed or disconnected, it could just be informational. In fact when the server first starts up, it immediately logs " server running on stdio" to stderr. - this.appendErrorMessage(connection, errorOutput) - // Only need to update webview right away if it's already disconnected - if (connection.server.status === "disconnected") { - await this.notifyWebviewOfServerChanges() + const output = data.toString() + + // Check if this is a startup info message or a real error + const isStartupInfo = output.includes("server running") || output.includes("MCP server running") + + if (!isStartupInfo) { + // Only log and process real errors, ignore startup info messages + console.error(`Server "${name}" stderr:`, output) + const connection = this.connections.find((conn) => conn.server.name === name) + if (connection) { + // NOTE: we do not set server status to "disconnected" because stderr logs do not necessarily mean the server crashed or disconnected + this.appendErrorMessage(connection, output) + // Only need to update webview right away if it's already disconnected + if (connection.server.status === "disconnected") { + await this.notifyWebviewOfServerChanges() + } } } }) @@ -630,7 +639,12 @@ export class McpHub { // Update or add servers for (const [name, config] of Object.entries(newServers)) { - const currentConnection = this.connections.find((conn) => conn.server.name === name) + // Only consider connections that match the current source + const currentConnection = this.connections.find( + (conn) => + conn.server.name === name && + (conn.server.source === source || (!conn.server.source && source === "global")), + ) // Validate and transform the config let validatedConfig: z.infer @@ -735,20 +749,49 @@ export class McpHub { } private async notifyWebviewOfServerChanges(): Promise { - // servers should always be sorted in the order they are defined in the settings file + // Get global server order from settings file const settingsPath = await this.getMcpSettingsFilePath() const content = await fs.readFile(settingsPath, "utf-8") const config = JSON.parse(content) - const serverOrder = Object.keys(config.mcpServers || {}) + const globalServerOrder = Object.keys(config.mcpServers || {}) + + // Get project server order if available + const projectMcpPath = await this.getProjectMcpPath() + let projectServerOrder: string[] = [] + if (projectMcpPath) { + try { + const projectContent = await fs.readFile(projectMcpPath, "utf-8") + const projectConfig = JSON.parse(projectContent) + projectServerOrder = Object.keys(projectConfig.mcpServers || {}) + } catch (error) { + console.error("Failed to read project MCP config:", error) + } + } + + // Sort connections: first global servers in their defined order, then project servers in their defined order + const sortedConnections = [...this.connections].sort((a, b) => { + const aIsGlobal = a.server.source === "global" || !a.server.source + const bIsGlobal = b.server.source === "global" || !b.server.source + + // If both are global or both are project, sort by their respective order + if (aIsGlobal && bIsGlobal) { + const indexA = globalServerOrder.indexOf(a.server.name) + const indexB = globalServerOrder.indexOf(b.server.name) + return indexA - indexB + } else if (!aIsGlobal && !bIsGlobal) { + const indexA = projectServerOrder.indexOf(a.server.name) + const indexB = projectServerOrder.indexOf(b.server.name) + return indexA - indexB + } + + // Global servers come before project servers + return aIsGlobal ? -1 : 1 + }) + + // Send sorted servers to webview await this.providerRef.deref()?.postMessageToWebview({ type: "mcpServers", - mcpServers: [...this.connections] - .sort((a, b) => { - const indexA = serverOrder.indexOf(a.server.name) - const indexB = serverOrder.indexOf(b.server.name) - return indexA - indexB - }) - .map((connection) => connection.server), + mcpServers: sortedConnections.map((connection) => connection.server), }) } From 6d70b8aeba1b240eddaf7f28fbb05d4da858bd73 Mon Sep 17 00:00:00 2001 From: aheizi Date: Tue, 18 Mar 2025 00:23:40 +0800 Subject: [PATCH 7/7] fix translations --- webview-ui/src/i18n/locales/vi/mcp.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/webview-ui/src/i18n/locales/vi/mcp.json b/webview-ui/src/i18n/locales/vi/mcp.json index 37f16cbec49..1252094da4e 100644 --- a/webview-ui/src/i18n/locales/vi/mcp.json +++ b/webview-ui/src/i18n/locales/vi/mcp.json @@ -10,6 +10,8 @@ "title": "Bật tạo máy chủ MCP", "description": "Khi được bật, Roo có thể giúp bạn tạo máy chủ MCP mới thông qua các lệnh như \"thêm công cụ mới để...\". Nếu bạn không cần tạo máy chủ MCP, bạn có thể tắt tính năng này để giảm lượng token mà Roo sử dụng." }, + "editGlobalMCP": "Chỉnh sửa MCP toàn cục", + "editProjectMCP": "Chỉnh sửa MCP dự án", "editSettings": "Chỉnh sửa cài đặt MCP", "tool": { "alwaysAllow": "Luôn cho phép",