From c7ac0c5db547edc500740af55a87601ef3dec411 Mon Sep 17 00:00:00 2001 From: aheizi Date: Thu, 27 Mar 2025 12:23:39 +0800 Subject: [PATCH] Support project level mcp (#1841) * support project-level mcp config * switch the toasts to English fix test (cherry picked from commit 26941dcaae32554a9b83d5935d05ca895d0006ed) * add i18n for project mcp (cherry picked from commit 792a8225c15a01ec5dc8cab660c5a2bfa8944b3c) * optimize McpHub.ts * fix merge main into head * fix project mcp * partial update mcp config * fix toggleToolAlwaysAllow * Modify mcp to support project and global of the same name * fix ut * remove unused mcp log * i18n for mcp * Revert README changes --------- Co-authored-by: Matt Rubens --- src/core/webview/ClineProvider.ts | 51 +- .../webview/__tests__/ClineProvider.test.ts | 124 +++ src/i18n/locales/ca/common.json | 1 + src/i18n/locales/de/common.json | 1 + src/i18n/locales/en/common.json | 3 +- src/i18n/locales/es/common.json | 1 + src/i18n/locales/fr/common.json | 1 + src/i18n/locales/hi/common.json | 1 + src/i18n/locales/it/common.json | 1 + src/i18n/locales/ja/common.json | 1 + src/i18n/locales/ko/common.json | 1 + src/i18n/locales/pl/common.json | 1 + src/i18n/locales/pt-BR/common.json | 1 + src/i18n/locales/tr/common.json | 1 + src/i18n/locales/vi/common.json | 1 + src/i18n/locales/zh-CN/common.json | 1 + src/i18n/locales/zh-TW/common.json | 1 + src/services/mcp/McpHub.ts | 740 ++++++++++++------ src/services/mcp/__tests__/McpHub.test.ts | 28 +- src/shared/WebviewMessage.ts | 1 + src/shared/mcp.ts | 2 + webview-ui/src/components/mcp/McpToolRow.tsx | 5 +- webview-ui/src/components/mcp/McpView.tsx | 48 +- .../mcp/__tests__/McpToolRow.test.tsx | 1 + webview-ui/src/i18n/locales/ca/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/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/tr/mcp.json | 3 +- webview-ui/src/i18n/locales/vi/mcp.json | 2 + webview-ui/src/i18n/locales/zh-CN/mcp.json | 2 + webview-ui/src/i18n/locales/zh-TW/mcp.json | 2 + 39 files changed, 800 insertions(+), 259 deletions(-) diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 3b1987e5bb..5f51b3f9e9 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -1249,6 +1249,28 @@ export class ClineProvider extends EventEmitter implements } break } + case "openProjectMcpSettings": { + if (!vscode.workspace.workspaceFolders?.length) { + vscode.window.showErrorMessage(t("common:no_workspace")) + 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(t("common:errors.create_mcp_json", { error: `${error}` })) + } + break + } case "openCustomModesSettings": { const customModesFilePath = await this.customModesManager.getCustomModesFilePath() if (customModesFilePath) { @@ -1263,7 +1285,7 @@ export class ClineProvider extends EventEmitter implements try { this.outputChannel.appendLine(`Attempting to delete MCP server: ${message.serverName}`) - await this.mcpHub?.deleteServer(message.serverName) + await this.mcpHub?.deleteServer(message.serverName, message.source as "global" | "project") this.outputChannel.appendLine(`Successfully deleted MCP server: ${message.serverName}`) } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error) @@ -1274,7 +1296,7 @@ export class ClineProvider extends EventEmitter implements } case "restartMcpServer": { try { - await this.mcpHub?.restartConnection(message.text!) + await this.mcpHub?.restartConnection(message.text!, message.source as "global" | "project") } catch (error) { this.outputChannel.appendLine( `Failed to retry connection for ${message.text}: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`, @@ -1284,11 +1306,14 @@ export class ClineProvider extends EventEmitter implements } case "toggleToolAlwaysAllow": { try { - await this.mcpHub?.toggleToolAlwaysAllow( - message.serverName!, - message.toolName!, - message.alwaysAllow!, - ) + if (this.mcpHub) { + await this.mcpHub.toggleToolAlwaysAllow( + message.serverName!, + message.source as "global" | "project", + message.toolName!, + Boolean(message.alwaysAllow), + ) + } } catch (error) { this.outputChannel.appendLine( `Failed to toggle auto-approve for tool ${message.toolName}: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`, @@ -1298,7 +1323,11 @@ export class ClineProvider extends EventEmitter implements } case "toggleMcpServer": { try { - await this.mcpHub?.toggleServerDisabled(message.serverName!, message.disabled!) + await this.mcpHub?.toggleServerDisabled( + message.serverName!, + message.disabled!, + message.source as "global" | "project", + ) } catch (error) { this.outputChannel.appendLine( `Failed to toggle MCP server ${message.serverName}: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`, @@ -2018,7 +2047,11 @@ export class ClineProvider extends EventEmitter implements case "updateMcpTimeout": if (message.serverName && typeof message.timeout === "number") { try { - await this.mcpHub?.updateServerTimeout(message.serverName, message.timeout) + await this.mcpHub?.updateServerTimeout( + message.serverName, + message.timeout, + message.source as "global" | "project", + ) } catch (error) { this.outputChannel.appendLine( `Failed to update timeout for ${message.serverName}: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`, diff --git a/src/core/webview/__tests__/ClineProvider.test.ts b/src/core/webview/__tests__/ClineProvider.test.ts index b1b2381594..e185738daa 100644 --- a/src/core/webview/__tests__/ClineProvider.test.ts +++ b/src/core/webview/__tests__/ClineProvider.test.ts @@ -2061,6 +2061,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/i18n/locales/ca/common.json b/src/i18n/locales/ca/common.json index 8147258595..633b90ec3d 100644 --- a/src/i18n/locales/ca/common.json +++ b/src/i18n/locales/ca/common.json @@ -48,6 +48,7 @@ "delete_api_config": "Ha fallat l'eliminació de la configuració de l'API", "list_api_config": "Ha fallat l'obtenció de la llista de configuracions de l'API", "update_server_timeout": "Ha fallat l'actualització del temps d'espera del servidor", + "failed_update_project_mcp": "Ha fallat l'actualització dels servidors MCP del projecte", "create_mcp_json": "Ha fallat la creació o obertura de .roo/mcp.json: {{error}}", "hmr_not_running": "El servidor de desenvolupament local no està executant-se, l'HMR no funcionarà. Si us plau, executa 'npm run dev' abans de llançar l'extensió per habilitar l'HMR.", "retrieve_current_mode": "Error en recuperar el mode actual de l'estat.", diff --git a/src/i18n/locales/de/common.json b/src/i18n/locales/de/common.json index 0b004641c8..ceda64bad7 100644 --- a/src/i18n/locales/de/common.json +++ b/src/i18n/locales/de/common.json @@ -44,6 +44,7 @@ "delete_api_config": "Fehler beim Löschen der API-Konfiguration", "list_api_config": "Fehler beim Abrufen der API-Konfigurationsliste", "update_server_timeout": "Fehler beim Aktualisieren des Server-Timeouts", + "failed_update_project_mcp": "Fehler beim Aktualisieren der Projekt-MCP-Server", "create_mcp_json": "Fehler beim Erstellen oder Öffnen von .roo/mcp.json: {{error}}", "hmr_not_running": "Der lokale Entwicklungsserver läuft nicht, HMR wird nicht funktionieren. Bitte führen Sie 'npm run dev' vor dem Start der Erweiterung aus, um HMR zu aktivieren.", "retrieve_current_mode": "Fehler beim Abrufen des aktuellen Modus aus dem Zustand.", diff --git a/src/i18n/locales/en/common.json b/src/i18n/locales/en/common.json index 0f964f8adf..ecd5a4c413 100644 --- a/src/i18n/locales/en/common.json +++ b/src/i18n/locales/en/common.json @@ -50,7 +50,8 @@ "failed_delete_repo": "Failed to delete associated shadow repository or branch: {{error}}", "failed_remove_directory": "Failed to remove task directory: {{error}}", "custom_storage_path_unusable": "Custom storage path \"{{path}}\" is unusable, will use default path", - "cannot_access_path": "Cannot access path {{path}}: {{error}}" + "cannot_access_path": "Cannot access path {{path}}: {{error}}", + "failed_update_project_mcp": "Failed to update project MCP servers" }, "warnings": { "no_terminal_content": "No terminal content selected", diff --git a/src/i18n/locales/es/common.json b/src/i18n/locales/es/common.json index 4b57081f6c..2bfb43055a 100644 --- a/src/i18n/locales/es/common.json +++ b/src/i18n/locales/es/common.json @@ -44,6 +44,7 @@ "delete_api_config": "Error al eliminar la configuración de API", "list_api_config": "Error al obtener la lista de configuraciones de API", "update_server_timeout": "Error al actualizar el tiempo de espera del servidor", + "failed_update_project_mcp": "Error al actualizar los servidores MCP del proyecto", "create_mcp_json": "Error al crear o abrir .roo/mcp.json: {{error}}", "hmr_not_running": "El servidor de desarrollo local no está en ejecución, HMR no funcionará. Por favor, ejecuta 'npm run dev' antes de lanzar la extensión para habilitar HMR.", "retrieve_current_mode": "Error al recuperar el modo actual del estado.", diff --git a/src/i18n/locales/fr/common.json b/src/i18n/locales/fr/common.json index 56a52ee83d..7399432c6a 100644 --- a/src/i18n/locales/fr/common.json +++ b/src/i18n/locales/fr/common.json @@ -44,6 +44,7 @@ "delete_api_config": "Erreur lors de la suppression de la configuration API", "list_api_config": "Erreur lors de l'obtention de la liste des configurations API", "update_server_timeout": "Erreur lors de la mise à jour du délai d'attente du serveur", + "failed_update_project_mcp": "Échec de la mise à jour des serveurs MCP du projet", "create_mcp_json": "Échec de la création ou de l'ouverture de .roo/mcp.json : {{error}}", "hmr_not_running": "Le serveur de développement local n'est pas en cours d'exécution, HMR ne fonctionnera pas. Veuillez exécuter 'npm run dev' avant de lancer l'extension pour activer l'HMR.", "retrieve_current_mode": "Erreur lors de la récupération du mode actuel à partir du state.", diff --git a/src/i18n/locales/hi/common.json b/src/i18n/locales/hi/common.json index 852e2a58c1..bf5421eff8 100644 --- a/src/i18n/locales/hi/common.json +++ b/src/i18n/locales/hi/common.json @@ -44,6 +44,7 @@ "delete_api_config": "API कॉन्फ़िगरेशन हटाने में विफल", "list_api_config": "API कॉन्फ़िगरेशन की सूची प्राप्त करने में विफल", "update_server_timeout": "सर्वर टाइमआउट अपडेट करने में विफल", + "failed_update_project_mcp": "प्रोजेक्ट MCP सर्वर अपडेट करने में विफल", "create_mcp_json": ".roo/mcp.json बनाने या खोलने में विफल: {{error}}", "hmr_not_running": "स्थानीय विकास सर्वर चल नहीं रहा है, HMR काम नहीं करेगा। कृपया HMR सक्षम करने के लिए एक्सटेंशन लॉन्च करने से पहले 'npm run dev' चलाएँ।", "retrieve_current_mode": "स्टेट से वर्तमान मोड प्राप्त करने में त्रुटि।", diff --git a/src/i18n/locales/it/common.json b/src/i18n/locales/it/common.json index a919d1bdb9..69e2c2123f 100644 --- a/src/i18n/locales/it/common.json +++ b/src/i18n/locales/it/common.json @@ -44,6 +44,7 @@ "delete_api_config": "Errore durante l'eliminazione della configurazione API", "list_api_config": "Errore durante l'ottenimento dell'elenco delle configurazioni API", "update_server_timeout": "Errore durante l'aggiornamento del timeout del server", + "failed_update_project_mcp": "Errore durante l'aggiornamento dei server MCP del progetto", "create_mcp_json": "Impossibile creare o aprire .roo/mcp.json: {{error}}", "hmr_not_running": "Il server di sviluppo locale non è in esecuzione, l'HMR non funzionerà. Esegui 'npm run dev' prima di avviare l'estensione per abilitare l'HMR.", "retrieve_current_mode": "Errore durante il recupero della modalità corrente dallo stato.", diff --git a/src/i18n/locales/ja/common.json b/src/i18n/locales/ja/common.json index 6bb6def7a8..6f40c8e03d 100644 --- a/src/i18n/locales/ja/common.json +++ b/src/i18n/locales/ja/common.json @@ -44,6 +44,7 @@ "delete_api_config": "API設定の削除に失敗しました", "list_api_config": "API設定リストの取得に失敗しました", "update_server_timeout": "サーバータイムアウトの更新に失敗しました", + "failed_update_project_mcp": "プロジェクトMCPサーバーの更新に失敗しました", "create_mcp_json": ".roo/mcp.jsonの作成または開くことに失敗しました:{{error}}", "hmr_not_running": "ローカル開発サーバーが実行されていないため、HMRは機能しません。HMRを有効にするには、拡張機能を起動する前に'npm run dev'を実行してください。", "retrieve_current_mode": "現在のモードを状態から取得する際にエラーが発生しました。", diff --git a/src/i18n/locales/ko/common.json b/src/i18n/locales/ko/common.json index 24f03a778b..9026315da2 100644 --- a/src/i18n/locales/ko/common.json +++ b/src/i18n/locales/ko/common.json @@ -44,6 +44,7 @@ "delete_api_config": "API 구성 삭제에 실패했습니다", "list_api_config": "API 구성 목록 가져오기에 실패했습니다", "update_server_timeout": "서버 타임아웃 업데이트에 실패했습니다", + "failed_update_project_mcp": "프로젝트 MCP 서버 업데이트에 실패했습니다", "create_mcp_json": ".roo/mcp.json 생성 또는 열기 실패: {{error}}", "hmr_not_running": "로컬 개발 서버가 실행되고 있지 않아 HMR이 작동하지 않습니다. HMR을 활성화하려면 확장 프로그램을 실행하기 전에 'npm run dev'를 실행하세요.", "retrieve_current_mode": "상태에서 현재 모드를 검색하는 데 오류가 발생했습니다.", diff --git a/src/i18n/locales/pl/common.json b/src/i18n/locales/pl/common.json index dd4e385e67..49f51cefff 100644 --- a/src/i18n/locales/pl/common.json +++ b/src/i18n/locales/pl/common.json @@ -44,6 +44,7 @@ "delete_api_config": "Nie udało się usunąć konfiguracji API", "list_api_config": "Nie udało się pobrać listy konfiguracji API", "update_server_timeout": "Nie udało się zaktualizować limitu czasu serwera", + "failed_update_project_mcp": "Nie udało się zaktualizować serwerów MCP projektu", "create_mcp_json": "Nie udało się utworzyć lub otworzyć .roo/mcp.json: {{error}}", "hmr_not_running": "Lokalny serwer deweloperski nie jest uruchomiony, HMR nie będzie działać. Uruchom 'npm run dev' przed uruchomieniem rozszerzenia, aby włączyć HMR.", "retrieve_current_mode": "Błąd podczas pobierania bieżącego trybu ze stanu.", diff --git a/src/i18n/locales/pt-BR/common.json b/src/i18n/locales/pt-BR/common.json index 9e2db6f7a6..80112a91ab 100644 --- a/src/i18n/locales/pt-BR/common.json +++ b/src/i18n/locales/pt-BR/common.json @@ -48,6 +48,7 @@ "delete_api_config": "Falha ao excluir a configuração da API", "list_api_config": "Falha ao obter a lista de configurações da API", "update_server_timeout": "Falha ao atualizar o tempo limite do servidor", + "failed_update_project_mcp": "Falha ao atualizar os servidores MCP do projeto", "create_mcp_json": "Falha ao criar ou abrir .roo/mcp.json: {{error}}", "hmr_not_running": "O servidor de desenvolvimento local não está em execução, o HMR não funcionará. Por favor, execute 'npm run dev' antes de iniciar a extensão para habilitar o HMR.", "retrieve_current_mode": "Erro ao recuperar o modo atual do estado.", diff --git a/src/i18n/locales/tr/common.json b/src/i18n/locales/tr/common.json index 3413057693..61b8e12fb5 100644 --- a/src/i18n/locales/tr/common.json +++ b/src/i18n/locales/tr/common.json @@ -44,6 +44,7 @@ "delete_api_config": "API yapılandırması silinemedi", "list_api_config": "API yapılandırma listesi alınamadı", "update_server_timeout": "Sunucu zaman aşımı güncellenemedi", + "failed_update_project_mcp": "Proje MCP sunucuları güncellenemedi", "create_mcp_json": ".roo/mcp.json oluşturulamadı veya açılamadı: {{error}}", "hmr_not_running": "Yerel geliştirme sunucusu çalışmıyor, HMR çalışmayacak. HMR'yi etkinleştirmek için uzantıyı başlatmadan önce lütfen 'npm run dev' komutunu çalıştırın.", "retrieve_current_mode": "Mevcut mod durumdan alınırken hata oluştu.", diff --git a/src/i18n/locales/vi/common.json b/src/i18n/locales/vi/common.json index 9e0bde90b1..8945e9e098 100644 --- a/src/i18n/locales/vi/common.json +++ b/src/i18n/locales/vi/common.json @@ -44,6 +44,7 @@ "delete_api_config": "Không thể xóa cấu hình API", "list_api_config": "Không thể lấy danh sách cấu hình API", "update_server_timeout": "Không thể cập nhật thời gian chờ máy chủ", + "failed_update_project_mcp": "Không thể cập nhật máy chủ MCP của dự án", "create_mcp_json": "Không thể tạo hoặc mở .roo/mcp.json: {{error}}", "hmr_not_running": "Máy chủ phát triển cục bộ không chạy, HMR sẽ không hoạt động. Vui lòng chạy 'npm run dev' trước khi khởi chạy tiện ích mở rộng để bật HMR.", "retrieve_current_mode": "Lỗi không thể truy xuất chế độ hiện tại từ trạng thái.", diff --git a/src/i18n/locales/zh-CN/common.json b/src/i18n/locales/zh-CN/common.json index 0b8e679580..2fc49c9b37 100644 --- a/src/i18n/locales/zh-CN/common.json +++ b/src/i18n/locales/zh-CN/common.json @@ -44,6 +44,7 @@ "delete_api_config": "删除API配置失败", "list_api_config": "获取API配置列表失败", "update_server_timeout": "更新服务器超时设置失败", + "failed_update_project_mcp": "更新项目MCP服务器失败", "create_mcp_json": "创建或打开 .roo/mcp.json 失败:{{error}}", "hmr_not_running": "本地开发服务器未运行,HMR将不起作用。请在启动扩展前运行'npm run dev'以启用HMR。", "retrieve_current_mode": "从状态中检索当前模式失败。", diff --git a/src/i18n/locales/zh-TW/common.json b/src/i18n/locales/zh-TW/common.json index 19d1e3bdbc..be0f523507 100644 --- a/src/i18n/locales/zh-TW/common.json +++ b/src/i18n/locales/zh-TW/common.json @@ -44,6 +44,7 @@ "delete_api_config": "刪除API配置失敗", "list_api_config": "獲取API配置列表失敗", "update_server_timeout": "更新服務器超時設置失敗", + "failed_update_project_mcp": "更新項目MCP服務器失敗", "create_mcp_json": "創建或打開 .roo/mcp.json 失敗:{{error}}", "hmr_not_running": "本地開發服務器未運行,HMR將不起作用。請在啟動擴展前運行'npm run dev'以啟用HMR。", "retrieve_current_mode": "從狀態中檢索當前模式失敗。", diff --git a/src/services/mcp/McpHub.ts b/src/services/mcp/McpHub.ts index 9c52787f4d..95b13f45cf 100644 --- a/src/services/mcp/McpHub.ts +++ b/src/services/mcp/McpHub.ts @@ -104,6 +104,7 @@ export class McpHub { private disposables: vscode.Disposable[] = [] private settingsWatcher?: vscode.FileSystemWatcher private fileWatchers: Map = new Map() + private projectMcpWatcher?: vscode.FileSystemWatcher private isDisposed: boolean = false connections: McpConnection[] = [] isConnecting: boolean = false @@ -111,7 +112,10 @@ export class McpHub { constructor(provider: ClineProvider) { this.providerRef = new WeakRef(provider) this.watchMcpSettingsFile() - this.initializeMcpServers() + this.watchProjectMcpFile() + this.setupWorkspaceFoldersWatcher() + this.initializeGlobalMcpServers() + this.initializeProjectMcpServers() } /** @@ -184,6 +188,98 @@ export class McpHub { // } } + 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() + this.watchProjectMcpFile() + }), + ) + } + + private async handleConfigFileChange(filePath: string, source: "global" | "project"): Promise { + try { + const content = await fs.readFile(filePath, "utf-8") + const config = JSON.parse(content) + const result = McpSettingsSchema.safeParse(config) + + if (!result.success) { + const errorMessages = result.error.errors + .map((err) => `${err.path.join(".")}: ${err.message}`) + .join("\n") + vscode.window.showErrorMessage(t("common:errors.invalid_mcp_settings_validation", { errorMessages })) + return + } + + await this.updateServerConnections(result.data.mcpServers || {}, source) + } catch (error) { + if (error instanceof SyntaxError) { + vscode.window.showErrorMessage(t("common:errors.invalid_mcp_settings_format")) + } else { + this.showErrorMessage(`Failed to process ${source} MCP settings change`, error) + } + } + } + + private watchProjectMcpFile(): void { + this.disposables.push( + vscode.workspace.onDidSaveTextDocument(async (document) => { + const projectMcpPath = await this.getProjectMcpPath() + if (projectMcpPath && arePathsEqual(document.uri.fsPath, projectMcpPath)) { + await this.handleConfigFileChange(projectMcpPath, "project") + } + }), + ) + } + + private async updateProjectMcpServers(): Promise { + try { + const projectMcpPath = await this.getProjectMcpPath() + if (!projectMcpPath) return + + const content = await fs.readFile(projectMcpPath, "utf-8") + let config: any + + try { + config = JSON.parse(content) + } catch (parseError) { + const errorMessage = t("common:errors.invalid_mcp_settings_syntax") + console.error(errorMessage, parseError) + vscode.window.showErrorMessage(errorMessage) + return + } + + // Validate configuration structure + const result = McpSettingsSchema.safeParse(config) + if (result.success) { + await this.updateServerConnections(result.data.mcpServers || {}, "project") + } else { + // Format validation errors for better user feedback + const errorMessages = result.error.errors + .map((err) => `${err.path.join(".")}: ${err.message}`) + .join("\n") + console.error("Invalid project MCP settings format:", errorMessages) + vscode.window.showErrorMessage(t("common:errors.invalid_mcp_settings_validation", { errorMessages })) + } + } catch (error) { + this.showErrorMessage(t("common:errors.failed_update_project_mcp"), error) + } + } + + 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, "project") + } + + await this.notifyWebviewOfServerChanges() + } + getServers(): McpServer[] { // Only return enabled servers return this.connections.filter((conn) => !conn.server.disabled).map((conn) => conn.server) @@ -231,77 +327,88 @@ export class McpHub { this.disposables.push( vscode.workspace.onDidSaveTextDocument(async (document) => { if (arePathsEqual(document.uri.fsPath, settingsPath)) { - const content = await fs.readFile(settingsPath, "utf-8") - const errorMessage = t("common:errors.invalid_mcp_settings_format") - let config: any - try { - config = JSON.parse(content) - } catch (error) { - vscode.window.showErrorMessage(errorMessage) - return - } - const result = McpSettingsSchema.safeParse(config) - if (!result.success) { - const errorMessages = result.error.errors - .map((err) => `${err.path.join(".")}: ${err.message}`) - .join("\n") - vscode.window.showErrorMessage( - t("common:errors.invalid_mcp_settings_validation", { errorMessages }), - ) - return - } - try { - await this.updateServerConnections(result.data.mcpServers || {}) - } catch (error) { - this.showErrorMessage("Failed to process MCP settings change", error) - } + await this.handleConfigFileChange(settingsPath, "global") } }), ) } - private async initializeMcpServers(): Promise { + private async initializeMcpServers(source: "global" | "project"): Promise { try { - const settingsPath = await this.getMcpSettingsFilePath() - const content = await fs.readFile(settingsPath, "utf-8") - let config: any + const configPath = + source === "global" ? await this.getMcpSettingsFilePath() : await this.getProjectMcpPath() - try { - config = JSON.parse(content) - } catch (parseError) { - const errorMessage = t("common:errors.invalid_mcp_settings_syntax") - console.error(errorMessage, parseError) - vscode.window.showErrorMessage(errorMessage) + if (!configPath) { return } - // Validate the config using McpSettingsSchema + const content = await fs.readFile(configPath, "utf-8") + const config = JSON.parse(content) const result = McpSettingsSchema.safeParse(config) + if (result.success) { - await this.updateServerConnections(result.data.mcpServers || {}) + await this.updateServerConnections(result.data.mcpServers || {}, source) } else { - // Format validation errors for better user feedback const errorMessages = result.error.errors .map((err) => `${err.path.join(".")}: ${err.message}`) .join("\n") - console.error("Invalid MCP settings format:", errorMessages) + console.error(`Invalid ${source} MCP settings format:`, errorMessages) vscode.window.showErrorMessage(t("common:errors.invalid_mcp_settings_validation", { errorMessages })) - // Still try to connect with the raw config, but show warnings - try { - await this.updateServerConnections(config.mcpServers || {}) - } catch (error) { - this.showErrorMessage("Failed to initialize MCP servers with raw config", error) + if (source === "global") { + // Still try to connect with the raw config, but show warnings + try { + await this.updateServerConnections(config.mcpServers || {}, source) + } catch (error) { + this.showErrorMessage(`Failed to initialize ${source} MCP servers with raw config`, error) + } } } } catch (error) { - this.showErrorMessage("Failed to initialize MCP servers", error) + if (error instanceof SyntaxError) { + const errorMessage = t("common:errors.invalid_mcp_settings_syntax") + console.error(errorMessage, error) + vscode.window.showErrorMessage(errorMessage) + } else { + this.showErrorMessage(`Failed to initialize ${source} MCP servers`, error) + } } } - private async connectToServer(name: string, config: z.infer): Promise { - // Remove existing connection if it exists - await this.deleteConnection(name) + private async initializeGlobalMcpServers(): Promise { + await this.initializeMcpServers("global") + } + + // 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 { + await this.initializeMcpServers("project") + } + + private async connectToServer( + name: string, + config: z.infer, + source: "global" | "project" = "global", + ): Promise { + // Remove existing connection if it exists with the same source + await this.deleteConnection(name, source) try { const client = new Client( @@ -330,7 +437,7 @@ export class McpHub { // Set up stdio specific error handling transport.onerror = async (error) => { console.error(`Transport error for "${name}":`, error) - const connection = this.connections.find((conn) => conn.server.name === name) + const connection = this.findConnection(name, source) if (connection) { connection.server.status = "disconnected" this.appendErrorMessage(connection, error instanceof Error ? error.message : `${error}`) @@ -339,7 +446,7 @@ export class McpHub { } transport.onclose = async () => { - const connection = this.connections.find((conn) => conn.server.name === name) + const connection = this.findConnection(name, source) if (connection) { connection.server.status = "disconnected" } @@ -362,7 +469,7 @@ export class McpHub { } else { // Treat as error log console.error(`Server "${name}" stderr:`, output) - const connection = this.connections.find((conn) => conn.server.name === name) + const connection = this.findConnection(name, source) if (connection) { this.appendErrorMessage(connection, output) if (connection.server.status === "disconnected") { @@ -396,7 +503,7 @@ export class McpHub { // Set up SSE specific error handling transport.onerror = async (error) => { console.error(`Transport error for "${name}":`, error) - const connection = this.connections.find((conn) => conn.server.name === name) + const connection = this.findConnection(name, source) if (connection) { connection.server.status = "disconnected" this.appendErrorMessage(connection, error instanceof Error ? error.message : `${error}`) @@ -411,6 +518,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, @@ -423,12 +532,12 @@ export class McpHub { connection.server.error = "" // Initial fetch of tools and resources - connection.server.tools = await this.fetchToolsList(name) - connection.server.resources = await this.fetchResourcesList(name) - connection.server.resourceTemplates = await this.fetchResourceTemplatesList(name) + connection.server.tools = await this.fetchToolsList(name, source) + connection.server.resources = await this.fetchResourcesList(name, source) + connection.server.resourceTemplates = await this.fetchResourceTemplatesList(name, source) } catch (error) { // Update status with error - const connection = this.connections.find((conn) => conn.server.name === name) + const connection = this.findConnection(name, source) if (connection) { connection.server.status = "disconnected" this.appendErrorMessage(connection, error instanceof Error ? error.message : `${error}`) @@ -438,26 +547,77 @@ export class McpHub { } private appendErrorMessage(connection: McpConnection, error: string) { - // Limit error message length to prevent excessive length - const maxErrorLength = 1000 + const MAX_ERROR_LENGTH = 1000 const newError = connection.server.error ? `${connection.server.error}\n${error}` : error connection.server.error = - newError.length > maxErrorLength - ? newError.substring(0, maxErrorLength) + "...(error message truncated)" + newError.length > MAX_ERROR_LENGTH + ? `${newError.substring(0, MAX_ERROR_LENGTH)}...(error message truncated)` : newError } - private async fetchToolsList(serverName: string): Promise { - try { - const response = await this.connections - .find((conn) => conn.server.name === serverName) - ?.client.request({ method: "tools/list" }, ListToolsResultSchema) + /** + * Helper method to find a connection by server name and source + * @param serverName The name of the server to find + * @param source Optional source to filter by (global or project) + * @returns The matching connection or undefined if not found + */ + private findConnection(serverName: string, source?: "global" | "project"): McpConnection | undefined { + // If source is specified, only find servers with that source + if (source !== undefined) { + return this.connections.find((conn) => conn.server.name === serverName && conn.server.source === source) + } - // Get always allow settings - const settingsPath = await this.getMcpSettingsFilePath() - const content = await fs.readFile(settingsPath, "utf-8") - const config = JSON.parse(content) - const alwaysAllowConfig = config.mcpServers[serverName]?.alwaysAllow || [] + // If no source is specified, first look for project servers, then global servers + // This ensures that when servers have the same name, project servers are prioritized + const projectConn = this.connections.find( + (conn) => conn.server.name === serverName && conn.server.source === "project", + ) + if (projectConn) return projectConn + + // If no project server is found, look for global servers + return this.connections.find( + (conn) => conn.server.name === serverName && (conn.server.source === "global" || !conn.server.source), + ) + } + + private async fetchToolsList(serverName: string, source?: "global" | "project"): Promise { + try { + // Use the helper method to find the connection + const connection = this.findConnection(serverName, source) + + if (!connection) { + throw new Error(`Server ${serverName} not found`) + } + + const response = await connection.client.request({ method: "tools/list" }, ListToolsResultSchema) + + // Determine the actual source of the server + const actualSource = connection.server.source || "global" + let configPath: string + let alwaysAllowConfig: string[] = [] + + // Read from the appropriate config file based on the actual source + try { + if (actualSource === "project") { + // Get project MCP config path + const projectMcpPath = await this.getProjectMcpPath() + if (projectMcpPath) { + configPath = projectMcpPath + const content = await fs.readFile(configPath, "utf-8") + const config = JSON.parse(content) + alwaysAllowConfig = config.mcpServers?.[serverName]?.alwaysAllow || [] + } + } else { + // Get global MCP settings path + configPath = await this.getMcpSettingsFilePath() + const content = await fs.readFile(configPath, "utf-8") + const config = JSON.parse(content) + alwaysAllowConfig = config.mcpServers?.[serverName]?.alwaysAllow || [] + } + } catch (error) { + console.error(`Failed to read alwaysAllow config for ${serverName}:`, error) + // Continue with empty alwaysAllowConfig + } // Mark tools as always allowed based on settings const tools = (response?.tools || []).map((tool) => ({ @@ -465,19 +625,20 @@ export class McpHub { alwaysAllow: alwaysAllowConfig.includes(tool.name), })) - console.log(`[MCP] Fetched tools for ${serverName}:`, tools) return tools } catch (error) { - // console.error(`Failed to fetch tools for ${serverName}:`, error) + console.error(`Failed to fetch tools for ${serverName}:`, error) return [] } } - private async fetchResourcesList(serverName: string): Promise { + private async fetchResourcesList(serverName: string, source?: "global" | "project"): Promise { try { - const response = await this.connections - .find((conn) => conn.server.name === serverName) - ?.client.request({ method: "resources/list" }, ListResourcesResultSchema) + const connection = this.findConnection(serverName, source) + if (!connection) { + return [] + } + const response = await connection.client.request({ method: "resources/list" }, ListResourcesResultSchema) return response?.resources || [] } catch (error) { // console.error(`Failed to fetch resources for ${serverName}:`, error) @@ -485,11 +646,19 @@ export class McpHub { } } - private async fetchResourceTemplatesList(serverName: string): Promise { + private async fetchResourceTemplatesList( + serverName: string, + source?: "global" | "project", + ): Promise { try { - const response = await this.connections - .find((conn) => conn.server.name === serverName) - ?.client.request({ method: "resources/templates/list" }, ListResourceTemplatesResultSchema) + const connection = this.findConnection(serverName, source) + if (!connection) { + return [] + } + const response = await connection.client.request( + { method: "resources/templates/list" }, + ListResourceTemplatesResultSchema, + ) return response?.resourceTemplates || [] } catch (error) { // console.error(`Failed to fetch resource templates for ${serverName}:`, error) @@ -497,36 +666,53 @@ export class McpHub { } } - async deleteConnection(name: string): Promise { - const connection = this.connections.find((conn) => conn.server.name === name) - if (connection) { + async deleteConnection(name: string, source?: "global" | "project"): Promise { + // If source is provided, only delete connections from that source + const connections = source + ? this.connections.filter((conn) => conn.server.name === name && conn.server.source === source) + : this.connections.filter((conn) => conn.server.name === name) + + for (const connection of connections) { try { await connection.transport.close() await connection.client.close() } catch (error) { console.error(`Failed to close transport for ${name}:`, error) } - this.connections = this.connections.filter((conn) => conn.server.name !== name) } + + // Remove the connections from the array + this.connections = this.connections.filter((conn) => { + if (conn.server.name !== name) return true + if (source && conn.server.source !== source) return true + return false + }) } - 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 for (const name of currentNames) { if (!newNames.has(name)) { - await this.deleteConnection(name) - console.log(`Deleted MCP server: ${name}`) + await this.deleteConnection(name, source) } } // 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.findConnection(name, source) // Validate and transform the config let validatedConfig: z.infer @@ -540,18 +726,17 @@ export class McpHub { if (!currentConnection) { // New server try { - this.setupFileWatcher(name, validatedConfig) - await this.connectToServer(name, validatedConfig) + this.setupFileWatcher(name, validatedConfig, source) + await this.connectToServer(name, validatedConfig, source) } catch (error) { this.showErrorMessage(`Failed to connect to new MCP server ${name}`, error) } } else if (!deepEqual(JSON.parse(currentConnection.server.config), config)) { // Existing server with changed config try { - this.setupFileWatcher(name, validatedConfig) - await this.deleteConnection(name) - await this.connectToServer(name, validatedConfig) - console.log(`Reconnected MCP server with updated config: ${name}`) + this.setupFileWatcher(name, validatedConfig, source) + await this.deleteConnection(name, source) + await this.connectToServer(name, validatedConfig, source) } catch (error) { this.showErrorMessage(`Failed to reconnect MCP server ${name}`, error) } @@ -562,7 +747,11 @@ export class McpHub { this.isConnecting = false } - private setupFileWatcher(name: string, config: z.infer) { + private setupFileWatcher( + name: string, + config: z.infer, + source: "global" | "project" = "global", + ) { // Initialize an empty array for this server if it doesn't exist if (!this.fileWatchers.has(name)) { this.fileWatchers.set(name, []) @@ -574,7 +763,6 @@ export class McpHub { if (config.type === "stdio") { // Setup watchers for custom watchPaths if defined if (config.watchPaths && config.watchPaths.length > 0) { - console.log(`Setting up custom path watchers for ${name} MCP server...`) const watchPathsWatcher = chokidar.watch(config.watchPaths, { // persistent: true, // ignoreInitial: true, @@ -582,9 +770,9 @@ export class McpHub { }) watchPathsWatcher.on("change", async (changedPath) => { - console.log(`Detected change in custom path ${changedPath}. Restarting server ${name}...`) try { - await this.restartConnection(name) + // Pass the source from the config to restartConnection + await this.restartConnection(name, source) } catch (error) { console.error(`Failed to restart server ${name} after change in ${changedPath}:`, error) } @@ -596,7 +784,6 @@ export class McpHub { // Also setup the fallback build/index.js watcher if applicable const filePath = config.args?.find((arg: string) => arg.includes("build/index.js")) if (filePath) { - console.log(`Setting up build/index.js watcher for ${name} MCP server...`) // we use chokidar instead of onDidSaveTextDocument because it doesn't require the file to be open in the editor const indexJsWatcher = chokidar.watch(filePath, { // persistent: true, @@ -605,9 +792,9 @@ export class McpHub { }) indexJsWatcher.on("change", async () => { - console.log(`Detected change in ${filePath}. Restarting server ${name}...`) try { - await this.restartConnection(name) + // Pass the source from the config to restartConnection + await this.restartConnection(name, source) } catch (error) { console.error(`Failed to restart server ${name} after change in ${filePath}:`, error) } @@ -628,7 +815,7 @@ export class McpHub { this.fileWatchers.clear() } - async restartConnection(serverName: string): Promise { + async restartConnection(serverName: string, source?: "global" | "project"): Promise { this.isConnecting = true const provider = this.providerRef.deref() if (!provider) { @@ -636,7 +823,7 @@ export class McpHub { } // Get existing connection and update its status - const connection = this.connections.find((conn) => conn.server.name === serverName) + const connection = this.findConnection(serverName, source) const config = connection?.server.config if (config) { vscode.window.showInformationMessage(t("common:info.mcp_server_restarting", { serverName })) @@ -645,7 +832,7 @@ export class McpHub { await this.notifyWebviewOfServerChanges() await delay(500) // artificial delay to show user that server is restarting try { - await this.deleteConnection(serverName) + await this.deleteConnection(serverName, connection.server.source) // Parse the config to validate it const parsedConfig = JSON.parse(config) try { @@ -653,7 +840,7 @@ export class McpHub { const validatedConfig = this.validateServerConfig(parsedConfig, serverName) // Try to connect again using validated config - await this.connectToServer(serverName, validatedConfig) + await this.connectToServer(serverName, validatedConfig, connection.server.source || "global") vscode.window.showInformationMessage(t("common:info.mcp_server_connected", { serverName })) } catch (validationError) { this.showErrorMessage(`Invalid configuration for MCP server "${serverName}"`, validationError) @@ -668,151 +855,219 @@ 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) { + // Silently continue with empty project server order + } + } + + // Sort connections: first project servers in their defined order, then global servers in their defined order + // This ensures that when servers have the same name, project servers are prioritized + 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 + } + + // Project servers come before global servers (reversed from original) + 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), }) } - public async toggleServerDisabled(serverName: string, disabled: boolean): Promise { - let settingsPath: string + public async toggleServerDisabled( + serverName: string, + disabled: boolean, + source?: "global" | "project", + ): Promise { try { - settingsPath = await this.getMcpSettingsFilePath() - - // Ensure the settings file exists and is accessible - try { - await fs.access(settingsPath) - } catch (error) { - console.error("Settings file not accessible:", error) - throw new Error("Settings file not accessible") - } - const content = await fs.readFile(settingsPath, "utf-8") - const config = JSON.parse(content) - - // Validate the config structure - if (!config || typeof config !== "object") { - throw new Error("Invalid config structure") + // Find the connection to determine if it's a global or project server + const connection = this.findConnection(serverName, source) + if (!connection) { + throw new Error(`Server ${serverName}${source ? ` with source ${source}` : ""} not found`) } - if (!config.mcpServers || typeof config.mcpServers !== "object") { - config.mcpServers = {} - } + const serverSource = connection.server.source || "global" + // Update the server config in the appropriate file + await this.updateServerConfig(serverName, { disabled }, serverSource) - if (config.mcpServers[serverName]) { - // Create a new server config object to ensure clean structure - const serverConfig = { - ...config.mcpServers[serverName], - disabled, - } + // Update the connection object + if (connection) { + try { + connection.server.disabled = disabled - // Ensure required fields exist - if (!serverConfig.alwaysAllow) { - serverConfig.alwaysAllow = [] - } - - config.mcpServers[serverName] = serverConfig - - // Write the entire config back - const updatedConfig = { - mcpServers: config.mcpServers, - } - - await fs.writeFile(settingsPath, JSON.stringify(updatedConfig, null, 2)) - - const connection = this.connections.find((conn) => conn.server.name === serverName) - if (connection) { - try { - connection.server.disabled = disabled - - // Only refresh capabilities if connected - if (connection.server.status === "connected") { - connection.server.tools = await this.fetchToolsList(serverName) - connection.server.resources = await this.fetchResourcesList(serverName) - connection.server.resourceTemplates = await this.fetchResourceTemplatesList(serverName) - } - } catch (error) { - console.error(`Failed to refresh capabilities for ${serverName}:`, error) + // Only refresh capabilities if connected + if (connection.server.status === "connected") { + connection.server.tools = await this.fetchToolsList(serverName, serverSource) + connection.server.resources = await this.fetchResourcesList(serverName, serverSource) + connection.server.resourceTemplates = await this.fetchResourceTemplatesList( + serverName, + serverSource, + ) } + } catch (error) { + console.error(`Failed to refresh capabilities for ${serverName}:`, error) } - - await this.notifyWebviewOfServerChanges() } + + await this.notifyWebviewOfServerChanges() } catch (error) { this.showErrorMessage(`Failed to update server ${serverName} state`, error) throw error } } - public async updateServerTimeout(serverName: string, timeout: number): Promise { - let settingsPath: string + /** + * Helper method to update a server's configuration in the appropriate settings file + * @param serverName The name of the server to update + * @param configUpdate The configuration updates to apply + * @param source Whether to update the global or project config + */ + private async updateServerConfig( + serverName: string, + configUpdate: Record, + source: "global" | "project" = "global", + ): Promise { + // Determine which config file to update + let configPath: string + if (source === "project") { + 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 try { - settingsPath = await this.getMcpSettingsFilePath() + await fs.access(configPath) + } catch (error) { + console.error("Settings file not accessible:", error) + throw new Error("Settings file not accessible") + } - // Ensure the settings file exists and is accessible - try { - await fs.access(settingsPath) - } catch (error) { - console.error("Settings file not accessible:", error) - throw new Error("Settings file not accessible") - } - const content = await fs.readFile(settingsPath, "utf-8") - const config = JSON.parse(content) + // Read and parse the config file + const content = await fs.readFile(configPath, "utf-8") + const config = JSON.parse(content) - // Validate the config structure - if (!config || typeof config !== "object") { - throw new Error("Invalid config structure") + // Validate the config structure + if (!config || typeof config !== "object") { + throw new Error("Invalid config structure") + } + + if (!config.mcpServers || typeof config.mcpServers !== "object") { + config.mcpServers = {} + } + + if (!config.mcpServers[serverName]) { + config.mcpServers[serverName] = {} + } + + // Create a new server config object to ensure clean structure + const serverConfig = { + ...config.mcpServers[serverName], + ...configUpdate, + } + + // Ensure required fields exist + if (!serverConfig.alwaysAllow) { + serverConfig.alwaysAllow = [] + } + + config.mcpServers[serverName] = serverConfig + + // Write the entire config back + const updatedConfig = { + mcpServers: config.mcpServers, + } + + await fs.writeFile(configPath, JSON.stringify(updatedConfig, null, 2)) + } + + public async updateServerTimeout( + serverName: string, + timeout: number, + source?: "global" | "project", + ): Promise { + try { + // Find the connection to determine if it's a global or project server + const connection = this.findConnection(serverName, source) + if (!connection) { + throw new Error(`Server ${serverName}${source ? ` with source ${source}` : ""} not found`) } - if (!config.mcpServers || typeof config.mcpServers !== "object") { - config.mcpServers = {} - } + // Update the server config in the appropriate file + await this.updateServerConfig(serverName, { timeout }, connection.server.source || "global") - if (config.mcpServers[serverName]) { - // Create a new server config object to ensure clean structure - const serverConfig = { - ...config.mcpServers[serverName], - timeout, - } - - config.mcpServers[serverName] = serverConfig - - // Write the entire config back - const updatedConfig = { - mcpServers: config.mcpServers, - } - - await fs.writeFile(settingsPath, JSON.stringify(updatedConfig, null, 2)) - await this.notifyWebviewOfServerChanges() - } + await this.notifyWebviewOfServerChanges() } catch (error) { this.showErrorMessage(`Failed to update server ${serverName} timeout settings`, error) throw error } } - public async deleteServer(serverName: string): Promise { + public async deleteServer(serverName: string, source?: "global" | "project"): Promise { try { - const settingsPath = await this.getMcpSettingsFilePath() + // Find the connection to determine if it's a global or project server + const connection = this.findConnection(serverName, source) + if (!connection) { + throw new Error(`Server ${serverName}${source ? ` with source ${source}` : ""} not found`) + } + + const serverSource = connection.server.source || "global" + // Determine config file based on server source + const isProjectServer = serverSource === "project" + let configPath: string + + if (isProjectServer) { + // Get project MCP config path + const projectMcpPath = await this.getProjectMcpPath() + if (!projectMcpPath) { + throw new Error("Project MCP configuration file not found") + } + configPath = projectMcpPath + } else { + // Get global MCP settings path + configPath = await this.getMcpSettingsFilePath() + } // Ensure the settings file exists and is accessible try { - await fs.access(settingsPath) + await fs.access(configPath) } catch (error) { throw new Error("Settings file not accessible") } - 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 @@ -833,10 +1088,10 @@ 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) + // Update server connections with the correct source + await this.updateServerConnections(config.mcpServers, serverSource) vscode.window.showInformationMessage(t("common:info.mcp_server_deleted", { serverName })) } else { @@ -848,10 +1103,10 @@ export class McpHub { } } - async readResource(serverName: string, uri: string): Promise { - const connection = this.connections.find((conn) => conn.server.name === serverName) + async readResource(serverName: string, uri: string, source?: "global" | "project"): Promise { + const connection = this.findConnection(serverName, source) if (!connection) { - throw new Error(`No connection found for server: ${serverName}`) + throw new Error(`No connection found for server: ${serverName}${source ? ` with source ${source}` : ""}`) } if (connection.server.disabled) { throw new Error(`Server "${serverName}" is disabled`) @@ -871,11 +1126,12 @@ export class McpHub { serverName: string, toolName: string, toolArguments?: Record, + source?: "global" | "project", ): Promise { - const connection = this.connections.find((conn) => conn.server.name === serverName) + const connection = this.findConnection(serverName, source) if (!connection) { throw new Error( - `No connection found for server: ${serverName}. Please make sure to use MCP servers available under 'Connected MCP Servers'.`, + `No connection found for server: ${serverName}${source ? ` with source ${source}` : ""}. Please make sure to use MCP servers available under 'Connected MCP Servers'.`, ) } if (connection.server.disabled) { @@ -907,12 +1163,48 @@ export class McpHub { ) } - async toggleToolAlwaysAllow(serverName: string, toolName: string, shouldAllow: boolean): Promise { + async toggleToolAlwaysAllow( + serverName: string, + source: "global" | "project", + toolName: string, + shouldAllow: boolean, + ): Promise { try { - const settingsPath = await this.getMcpSettingsFilePath() - const content = await fs.readFile(settingsPath, "utf-8") + // Find the connection with matching name and source + const connection = this.findConnection(serverName, source) + + if (!connection) { + throw new Error(`Server ${serverName} with source ${source} not found`) + } + + // Determine the correct config path based on the source + let configPath: string + if (source === "project") { + // Get project MCP config path + const projectMcpPath = await this.getProjectMcpPath() + if (!projectMcpPath) { + throw new Error("Project MCP configuration file not found") + } + configPath = projectMcpPath + } else { + // Get global MCP settings path + configPath = await this.getMcpSettingsFilePath() + } + + // Read the appropriate config file + const content = await fs.readFile(configPath, "utf-8") const config = JSON.parse(content) + // Initialize mcpServers if it doesn't exist + if (!config.mcpServers) { + config.mcpServers = {} + } + + // Initialize server config if it doesn't exist + if (!config.mcpServers[serverName]) { + config.mcpServers[serverName] = {} + } + // Initialize alwaysAllow if it doesn't exist if (!config.mcpServers[serverName].alwaysAllow) { config.mcpServers[serverName].alwaysAllow = [] @@ -930,12 +1222,12 @@ export class McpHub { } // Write updated config back to file - await fs.writeFile(settingsPath, JSON.stringify(config, null, 2)) + await fs.writeFile(configPath, JSON.stringify(config, null, 2)) // Update the tools list to reflect the change - const connection = this.connections.find((conn) => conn.server.name === serverName) if (connection) { - connection.server.tools = await this.fetchToolsList(serverName) + // Explicitly pass the source to ensure we're updating the correct server's tools + connection.server.tools = await this.fetchToolsList(serverName, source) await this.notifyWebviewOfServerChanges() } } catch (error) { @@ -949,7 +1241,7 @@ export class McpHub { this.removeAllFileWatchers() for (const connection of this.connections) { try { - await this.deleteConnection(connection.server.name) + await this.deleteConnection(connection.server.name, connection.server.source) } catch (error) { console.error(`Failed to close connection for ${connection.server.name}:`, error) } diff --git a/src/services/mcp/__tests__/McpHub.test.ts b/src/services/mcp/__tests__/McpHub.test.ts index 7fcce6662a..927f6b4e5d 100644 --- a/src/services/mcp/__tests__/McpHub.test.ts +++ b/src/services/mcp/__tests__/McpHub.test.ts @@ -7,7 +7,27 @@ import { ServerConfigSchema } 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") @@ -99,7 +119,7 @@ describe("McpHub", () => { // Mock reading initial config ;(fs.readFile as jest.Mock).mockResolvedValueOnce(JSON.stringify(mockConfig)) - await mcpHub.toggleToolAlwaysAllow("test-server", "new-tool", true) + await mcpHub.toggleToolAlwaysAllow("test-server", "global", "new-tool", true) // Verify the config was updated correctly const writeCall = (fs.writeFile as jest.Mock).mock.calls[0] @@ -122,7 +142,7 @@ describe("McpHub", () => { // Mock reading initial config ;(fs.readFile as jest.Mock).mockResolvedValueOnce(JSON.stringify(mockConfig)) - await mcpHub.toggleToolAlwaysAllow("test-server", "existing-tool", false) + await mcpHub.toggleToolAlwaysAllow("test-server", "global", "existing-tool", false) // Verify the config was updated correctly const writeCall = (fs.writeFile as jest.Mock).mock.calls[0] @@ -144,7 +164,7 @@ describe("McpHub", () => { // Mock reading initial config ;(fs.readFile as jest.Mock).mockResolvedValueOnce(JSON.stringify(mockConfig)) - await mcpHub.toggleToolAlwaysAllow("test-server", "new-tool", true) + await mcpHub.toggleToolAlwaysAllow("test-server", "global", "new-tool", true) // Verify the config was updated with initialized alwaysAllow const writeCall = (fs.writeFile as jest.Mock).mock.calls[0] diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts index 76f1667558..6b23d63b29 100644 --- a/src/shared/WebviewMessage.ts +++ b/src/shared/WebviewMessage.ts @@ -69,6 +69,7 @@ export interface WebviewMessage { | "screenshotQuality" | "remoteBrowserHost" | "openMcpSettings" + | "openProjectMcpSettings" | "restartMcpServer" | "toggleToolAlwaysAllow" | "toggleMcpServer" diff --git a/src/shared/mcp.ts b/src/shared/mcp.ts index 2bc38a12a8..7a490851bc 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/McpToolRow.tsx b/webview-ui/src/components/mcp/McpToolRow.tsx index 9f04ce1a2f..177ff8d223 100644 --- a/webview-ui/src/components/mcp/McpToolRow.tsx +++ b/webview-ui/src/components/mcp/McpToolRow.tsx @@ -6,17 +6,18 @@ import { vscode } from "../../utils/vscode" type McpToolRowProps = { tool: McpTool serverName?: string + serverSource?: "global" | "project" alwaysAllowMcp?: boolean } -const McpToolRow = ({ tool, serverName, alwaysAllowMcp }: McpToolRowProps) => { +const McpToolRow = ({ tool, serverName, serverSource, alwaysAllowMcp }: McpToolRowProps) => { const { t } = useAppTranslation() const handleAlwaysAllowChange = () => { if (!serverName) return - vscode.postMessage({ type: "toggleToolAlwaysAllow", serverName, + source: serverSource || "global", toolName: tool.name, alwaysAllow: !tool.alwaysAllow, }) diff --git a/webview-ui/src/components/mcp/McpView.tsx b/webview-ui/src/components/mcp/McpView.tsx index 97111e1332..a262d2b5e0 100644 --- a/webview-ui/src/components/mcp/McpView.tsx +++ b/webview-ui/src/components/mcp/McpView.tsx @@ -89,21 +89,34 @@ const McpView = ({ onDone }: McpViewProps) => { {servers.length > 0 && (
{servers.map((server) => ( - + ))}
)} - {/* Edit Settings Button */} -
+ {/* Edit Settings Buttons */} +
{ vscode.postMessage({ type: "openMcpSettings" }) }}> - {t("mcp:editSettings")} + {t("mcp:editGlobalMCP")} + + { + vscode.postMessage({ type: "openProjectMcpSettings" }) + }}> + + {t("mcp:editProjectMCP")}
@@ -154,6 +167,7 @@ const ServerRow = ({ server, alwaysAllowMcp }: { server: McpServer; alwaysAllowM vscode.postMessage({ type: "restartMcpServer", text: server.name, + source: server.source || "global", }) } @@ -163,6 +177,7 @@ const ServerRow = ({ server, alwaysAllowMcp }: { server: McpServer; alwaysAllowM vscode.postMessage({ type: "updateMcpTimeout", serverName: server.name, + source: server.source || "global", timeout: seconds, }) } @@ -171,6 +186,7 @@ const ServerRow = ({ server, alwaysAllowMcp }: { server: McpServer; alwaysAllowM vscode.postMessage({ type: "deleteMcpServer", serverName: server.name, + source: server.source || "global", }) setShowDeleteConfirm(false) } @@ -194,7 +210,22 @@ const ServerRow = ({ server, alwaysAllowMcp }: { server: McpServer; alwaysAllowM style={{ marginRight: "8px" }} /> )} - {server.name} + + {server.name} + {server.source && ( + + {server.source} + + )} +
e.stopPropagation()}> @@ -231,6 +262,7 @@ const ServerRow = ({ server, alwaysAllowMcp }: { server: McpServer; alwaysAllowM vscode.postMessage({ type: "toggleMcpServer", serverName: server.name, + source: server.source || "global", disabled: !server.disabled, }) }} @@ -240,6 +272,7 @@ const ServerRow = ({ server, alwaysAllowMcp }: { server: McpServer; alwaysAllowM vscode.postMessage({ type: "toggleMcpServer", serverName: server.name, + source: server.source || "global", disabled: !server.disabled, }) } @@ -321,9 +354,10 @@ const ServerRow = ({ server, alwaysAllowMcp }: { server: McpServer; alwaysAllowM style={{ display: "flex", flexDirection: "column", gap: "8px", width: "100%" }}> {server.tools.map((tool) => ( ))} diff --git a/webview-ui/src/components/mcp/__tests__/McpToolRow.test.tsx b/webview-ui/src/components/mcp/__tests__/McpToolRow.test.tsx index d280d881e3..0b0192ace2 100644 --- a/webview-ui/src/components/mcp/__tests__/McpToolRow.test.tsx +++ b/webview-ui/src/components/mcp/__tests__/McpToolRow.test.tsx @@ -83,6 +83,7 @@ describe("McpToolRow", () => { serverName: "test-server", toolName: "test-tool", alwaysAllow: true, + source: "global", }) }) diff --git a/webview-ui/src/i18n/locales/ca/mcp.json b/webview-ui/src/i18n/locales/ca/mcp.json index 1f339dd09f..8da2cbb953 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/de/mcp.json b/webview-ui/src/i18n/locales/de/mcp.json index 2455f20cd0..60332c27d6 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 dir helfen, neue MCP-Server über Befehle wie \"neues Tool hinzufügen zu...\" zu erstellen. Wenn du keine MCP-Server erstellen musst, kannst du 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 710b787e5d..95ec55bd0c 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 8cc199ad25..6b6c7eb198 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 2a239cd33e..2f3195067c 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 5827787b67..32aee49dc4 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/it/mcp.json b/webview-ui/src/i18n/locales/it/mcp.json index c297cca91a..18bc9b487e 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 62e340db84..d5920c1c46 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 f79409d441..5d45eb0fca 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 7308698d79..4f1f2a9411 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 e5713608eb..2c8c282e0d 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/tr/mcp.json b/webview-ui/src/i18n/locales/tr/mcp.json index f0610630c0..c21b009773 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/vi/mcp.json b/webview-ui/src/i18n/locales/vi/mcp.json index 37f16cbec4..1252094da4 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", diff --git a/webview-ui/src/i18n/locales/zh-CN/mcp.json b/webview-ui/src/i18n/locales/zh-CN/mcp.json index f57f419091..703ae43140 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 e8ddba2f2c..ee85661101 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": "始終允許",