Skip to content

Commit c7ac0c5

Browse files
aheizimrubens
andauthored
Support project level mcp (#1841)
* support project-level mcp config * switch the toasts to English fix test (cherry picked from commit 26941dc) * add i18n for project mcp (cherry picked from commit 792a822) * 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 <[email protected]>
1 parent c62e8f2 commit c7ac0c5

File tree

39 files changed

+795
-254
lines changed

39 files changed

+795
-254
lines changed

src/core/webview/ClineProvider.ts

Lines changed: 42 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1249,6 +1249,28 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
12491249
}
12501250
break
12511251
}
1252+
case "openProjectMcpSettings": {
1253+
if (!vscode.workspace.workspaceFolders?.length) {
1254+
vscode.window.showErrorMessage(t("common:no_workspace"))
1255+
return
1256+
}
1257+
1258+
const workspaceFolder = vscode.workspace.workspaceFolders[0]
1259+
const rooDir = path.join(workspaceFolder.uri.fsPath, ".roo")
1260+
const mcpPath = path.join(rooDir, "mcp.json")
1261+
1262+
try {
1263+
await fs.mkdir(rooDir, { recursive: true })
1264+
const exists = await fileExistsAtPath(mcpPath)
1265+
if (!exists) {
1266+
await fs.writeFile(mcpPath, JSON.stringify({ mcpServers: {} }, null, 2))
1267+
}
1268+
await openFile(mcpPath)
1269+
} catch (error) {
1270+
vscode.window.showErrorMessage(t("common:errors.create_mcp_json", { error: `${error}` }))
1271+
}
1272+
break
1273+
}
12521274
case "openCustomModesSettings": {
12531275
const customModesFilePath = await this.customModesManager.getCustomModesFilePath()
12541276
if (customModesFilePath) {
@@ -1263,7 +1285,7 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
12631285

12641286
try {
12651287
this.outputChannel.appendLine(`Attempting to delete MCP server: ${message.serverName}`)
1266-
await this.mcpHub?.deleteServer(message.serverName)
1288+
await this.mcpHub?.deleteServer(message.serverName, message.source as "global" | "project")
12671289
this.outputChannel.appendLine(`Successfully deleted MCP server: ${message.serverName}`)
12681290
} catch (error) {
12691291
const errorMessage = error instanceof Error ? error.message : String(error)
@@ -1274,7 +1296,7 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
12741296
}
12751297
case "restartMcpServer": {
12761298
try {
1277-
await this.mcpHub?.restartConnection(message.text!)
1299+
await this.mcpHub?.restartConnection(message.text!, message.source as "global" | "project")
12781300
} catch (error) {
12791301
this.outputChannel.appendLine(
12801302
`Failed to retry connection for ${message.text}: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,
@@ -1284,11 +1306,14 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
12841306
}
12851307
case "toggleToolAlwaysAllow": {
12861308
try {
1287-
await this.mcpHub?.toggleToolAlwaysAllow(
1288-
message.serverName!,
1289-
message.toolName!,
1290-
message.alwaysAllow!,
1291-
)
1309+
if (this.mcpHub) {
1310+
await this.mcpHub.toggleToolAlwaysAllow(
1311+
message.serverName!,
1312+
message.source as "global" | "project",
1313+
message.toolName!,
1314+
Boolean(message.alwaysAllow),
1315+
)
1316+
}
12921317
} catch (error) {
12931318
this.outputChannel.appendLine(
12941319
`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<ClineProviderEvents> implements
12981323
}
12991324
case "toggleMcpServer": {
13001325
try {
1301-
await this.mcpHub?.toggleServerDisabled(message.serverName!, message.disabled!)
1326+
await this.mcpHub?.toggleServerDisabled(
1327+
message.serverName!,
1328+
message.disabled!,
1329+
message.source as "global" | "project",
1330+
)
13021331
} catch (error) {
13031332
this.outputChannel.appendLine(
13041333
`Failed to toggle MCP server ${message.serverName}: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,
@@ -2018,7 +2047,11 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
20182047
case "updateMcpTimeout":
20192048
if (message.serverName && typeof message.timeout === "number") {
20202049
try {
2021-
await this.mcpHub?.updateServerTimeout(message.serverName, message.timeout)
2050+
await this.mcpHub?.updateServerTimeout(
2051+
message.serverName,
2052+
message.timeout,
2053+
message.source as "global" | "project",
2054+
)
20222055
} catch (error) {
20232056
this.outputChannel.appendLine(
20242057
`Failed to update timeout for ${message.serverName}: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,

src/core/webview/__tests__/ClineProvider.test.ts

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2061,6 +2061,130 @@ describe("ClineProvider", () => {
20612061
})
20622062
})
20632063

2064+
describe("Project MCP Settings", () => {
2065+
let provider: ClineProvider
2066+
let mockContext: vscode.ExtensionContext
2067+
let mockOutputChannel: vscode.OutputChannel
2068+
let mockWebviewView: vscode.WebviewView
2069+
let mockPostMessage: jest.Mock
2070+
2071+
beforeEach(() => {
2072+
jest.clearAllMocks()
2073+
2074+
mockContext = {
2075+
extensionPath: "/test/path",
2076+
extensionUri: {} as vscode.Uri,
2077+
globalState: {
2078+
get: jest.fn(),
2079+
update: jest.fn(),
2080+
keys: jest.fn().mockReturnValue([]),
2081+
},
2082+
secrets: {
2083+
get: jest.fn(),
2084+
store: jest.fn(),
2085+
delete: jest.fn(),
2086+
},
2087+
subscriptions: [],
2088+
extension: {
2089+
packageJSON: { version: "1.0.0" },
2090+
},
2091+
globalStorageUri: {
2092+
fsPath: "/test/storage/path",
2093+
},
2094+
} as unknown as vscode.ExtensionContext
2095+
2096+
mockOutputChannel = {
2097+
appendLine: jest.fn(),
2098+
clear: jest.fn(),
2099+
dispose: jest.fn(),
2100+
} as unknown as vscode.OutputChannel
2101+
2102+
mockPostMessage = jest.fn()
2103+
mockWebviewView = {
2104+
webview: {
2105+
postMessage: mockPostMessage,
2106+
html: "",
2107+
options: {},
2108+
onDidReceiveMessage: jest.fn(),
2109+
asWebviewUri: jest.fn(),
2110+
},
2111+
visible: true,
2112+
onDidDispose: jest.fn(),
2113+
onDidChangeVisibility: jest.fn(),
2114+
} as unknown as vscode.WebviewView
2115+
2116+
provider = new ClineProvider(mockContext, mockOutputChannel)
2117+
})
2118+
2119+
test("handles openProjectMcpSettings message", async () => {
2120+
await provider.resolveWebviewView(mockWebviewView)
2121+
const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
2122+
2123+
// Mock workspace folders
2124+
;(vscode.workspace as any).workspaceFolders = [{ uri: { fsPath: "/test/workspace" } }]
2125+
2126+
// Mock fs functions
2127+
const fs = require("fs/promises")
2128+
fs.mkdir.mockResolvedValue(undefined)
2129+
fs.writeFile.mockResolvedValue(undefined)
2130+
2131+
// Trigger openProjectMcpSettings
2132+
await messageHandler({
2133+
type: "openProjectMcpSettings",
2134+
})
2135+
2136+
// Verify directory was created
2137+
expect(fs.mkdir).toHaveBeenCalledWith(
2138+
expect.stringContaining(".roo"),
2139+
expect.objectContaining({ recursive: true }),
2140+
)
2141+
2142+
// Verify file was created with default content
2143+
expect(fs.writeFile).toHaveBeenCalledWith(
2144+
expect.stringContaining("mcp.json"),
2145+
JSON.stringify({ mcpServers: {} }, null, 2),
2146+
)
2147+
})
2148+
2149+
test("handles openProjectMcpSettings when workspace is not open", async () => {
2150+
await provider.resolveWebviewView(mockWebviewView)
2151+
const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
2152+
2153+
// Mock no workspace folders
2154+
;(vscode.workspace as any).workspaceFolders = []
2155+
2156+
// Trigger openProjectMcpSettings
2157+
await messageHandler({
2158+
type: "openProjectMcpSettings",
2159+
})
2160+
2161+
// Verify error message was shown
2162+
expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("Please open a project folder first")
2163+
})
2164+
2165+
test("handles openProjectMcpSettings file creation error", async () => {
2166+
await provider.resolveWebviewView(mockWebviewView)
2167+
const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
2168+
2169+
// Mock workspace folders
2170+
;(vscode.workspace as any).workspaceFolders = [{ uri: { fsPath: "/test/workspace" } }]
2171+
2172+
// Mock fs functions to fail
2173+
const fs = require("fs/promises")
2174+
fs.mkdir.mockRejectedValue(new Error("Failed to create directory"))
2175+
2176+
// Trigger openProjectMcpSettings
2177+
await messageHandler({
2178+
type: "openProjectMcpSettings",
2179+
})
2180+
2181+
// Verify error message was shown
2182+
expect(vscode.window.showErrorMessage).toHaveBeenCalledWith(
2183+
expect.stringContaining("Failed to create or open .roo/mcp.json"),
2184+
)
2185+
})
2186+
})
2187+
20642188
describe("ContextProxy integration", () => {
20652189
let provider: ClineProvider
20662190
let mockContext: vscode.ExtensionContext

src/i18n/locales/ca/common.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
"delete_api_config": "Ha fallat l'eliminació de la configuració de l'API",
4949
"list_api_config": "Ha fallat l'obtenció de la llista de configuracions de l'API",
5050
"update_server_timeout": "Ha fallat l'actualització del temps d'espera del servidor",
51+
"failed_update_project_mcp": "Ha fallat l'actualització dels servidors MCP del projecte",
5152
"create_mcp_json": "Ha fallat la creació o obertura de .roo/mcp.json: {{error}}",
5253
"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.",
5354
"retrieve_current_mode": "Error en recuperar el mode actual de l'estat.",

src/i18n/locales/de/common.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
"delete_api_config": "Fehler beim Löschen der API-Konfiguration",
4545
"list_api_config": "Fehler beim Abrufen der API-Konfigurationsliste",
4646
"update_server_timeout": "Fehler beim Aktualisieren des Server-Timeouts",
47+
"failed_update_project_mcp": "Fehler beim Aktualisieren der Projekt-MCP-Server",
4748
"create_mcp_json": "Fehler beim Erstellen oder Öffnen von .roo/mcp.json: {{error}}",
4849
"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.",
4950
"retrieve_current_mode": "Fehler beim Abrufen des aktuellen Modus aus dem Zustand.",

src/i18n/locales/en/common.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,8 @@
5050
"failed_delete_repo": "Failed to delete associated shadow repository or branch: {{error}}",
5151
"failed_remove_directory": "Failed to remove task directory: {{error}}",
5252
"custom_storage_path_unusable": "Custom storage path \"{{path}}\" is unusable, will use default path",
53-
"cannot_access_path": "Cannot access path {{path}}: {{error}}"
53+
"cannot_access_path": "Cannot access path {{path}}: {{error}}",
54+
"failed_update_project_mcp": "Failed to update project MCP servers"
5455
},
5556
"warnings": {
5657
"no_terminal_content": "No terminal content selected",

src/i18n/locales/es/common.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
"delete_api_config": "Error al eliminar la configuración de API",
4545
"list_api_config": "Error al obtener la lista de configuraciones de API",
4646
"update_server_timeout": "Error al actualizar el tiempo de espera del servidor",
47+
"failed_update_project_mcp": "Error al actualizar los servidores MCP del proyecto",
4748
"create_mcp_json": "Error al crear o abrir .roo/mcp.json: {{error}}",
4849
"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.",
4950
"retrieve_current_mode": "Error al recuperar el modo actual del estado.",

src/i18n/locales/fr/common.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
"delete_api_config": "Erreur lors de la suppression de la configuration API",
4545
"list_api_config": "Erreur lors de l'obtention de la liste des configurations API",
4646
"update_server_timeout": "Erreur lors de la mise à jour du délai d'attente du serveur",
47+
"failed_update_project_mcp": "Échec de la mise à jour des serveurs MCP du projet",
4748
"create_mcp_json": "Échec de la création ou de l'ouverture de .roo/mcp.json : {{error}}",
4849
"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.",
4950
"retrieve_current_mode": "Erreur lors de la récupération du mode actuel à partir du state.",

src/i18n/locales/hi/common.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
"delete_api_config": "API कॉन्फ़िगरेशन हटाने में विफल",
4545
"list_api_config": "API कॉन्फ़िगरेशन की सूची प्राप्त करने में विफल",
4646
"update_server_timeout": "सर्वर टाइमआउट अपडेट करने में विफल",
47+
"failed_update_project_mcp": "प्रोजेक्ट MCP सर्वर अपडेट करने में विफल",
4748
"create_mcp_json": ".roo/mcp.json बनाने या खोलने में विफल: {{error}}",
4849
"hmr_not_running": "स्थानीय विकास सर्वर चल नहीं रहा है, HMR काम नहीं करेगा। कृपया HMR सक्षम करने के लिए एक्सटेंशन लॉन्च करने से पहले 'npm run dev' चलाएँ।",
4950
"retrieve_current_mode": "स्टेट से वर्तमान मोड प्राप्त करने में त्रुटि।",

src/i18n/locales/it/common.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
"delete_api_config": "Errore durante l'eliminazione della configurazione API",
4545
"list_api_config": "Errore durante l'ottenimento dell'elenco delle configurazioni API",
4646
"update_server_timeout": "Errore durante l'aggiornamento del timeout del server",
47+
"failed_update_project_mcp": "Errore durante l'aggiornamento dei server MCP del progetto",
4748
"create_mcp_json": "Impossibile creare o aprire .roo/mcp.json: {{error}}",
4849
"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.",
4950
"retrieve_current_mode": "Errore durante il recupero della modalità corrente dallo stato.",

src/i18n/locales/ja/common.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
"delete_api_config": "API設定の削除に失敗しました",
4545
"list_api_config": "API設定リストの取得に失敗しました",
4646
"update_server_timeout": "サーバータイムアウトの更新に失敗しました",
47+
"failed_update_project_mcp": "プロジェクトMCPサーバーの更新に失敗しました",
4748
"create_mcp_json": ".roo/mcp.jsonの作成または開くことに失敗しました:{{error}}",
4849
"hmr_not_running": "ローカル開発サーバーが実行されていないため、HMRは機能しません。HMRを有効にするには、拡張機能を起動する前に'npm run dev'を実行してください。",
4950
"retrieve_current_mode": "現在のモードを状態から取得する際にエラーが発生しました。",

0 commit comments

Comments
 (0)