Skip to content

Commit df80e96

Browse files
authored
Merge pull request #1618 from aheizi/support_project_mcp
feat: support project-level mcp config
2 parents dc302f7 + 6d70b8a commit df80e96

File tree

22 files changed

+450
-60
lines changed

22 files changed

+450
-60
lines changed

src/core/webview/ClineProvider.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1118,6 +1118,28 @@ export class ClineProvider implements vscode.WebviewViewProvider {
11181118
}
11191119
break
11201120
}
1121+
case "openProjectMcpSettings": {
1122+
if (!vscode.workspace.workspaceFolders?.length) {
1123+
vscode.window.showErrorMessage("Please open a project folder first")
1124+
return
1125+
}
1126+
1127+
const workspaceFolder = vscode.workspace.workspaceFolders[0]
1128+
const rooDir = path.join(workspaceFolder.uri.fsPath, ".roo")
1129+
const mcpPath = path.join(rooDir, "mcp.json")
1130+
1131+
try {
1132+
await fs.mkdir(rooDir, { recursive: true })
1133+
const exists = await fileExistsAtPath(mcpPath)
1134+
if (!exists) {
1135+
await fs.writeFile(mcpPath, JSON.stringify({ mcpServers: {} }, null, 2))
1136+
}
1137+
await openFile(mcpPath)
1138+
} catch (error) {
1139+
vscode.window.showErrorMessage(`Failed to create or open .roo/mcp.json: ${error}`)
1140+
}
1141+
break
1142+
}
11211143
case "openCustomModesSettings": {
11221144
const customModesFilePath = await this.customModesManager.getCustomModesFilePath()
11231145
if (customModesFilePath) {

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

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1962,6 +1962,130 @@ describe("ClineProvider", () => {
19621962
})
19631963
})
19641964

1965+
describe("Project MCP Settings", () => {
1966+
let provider: ClineProvider
1967+
let mockContext: vscode.ExtensionContext
1968+
let mockOutputChannel: vscode.OutputChannel
1969+
let mockWebviewView: vscode.WebviewView
1970+
let mockPostMessage: jest.Mock
1971+
1972+
beforeEach(() => {
1973+
jest.clearAllMocks()
1974+
1975+
mockContext = {
1976+
extensionPath: "/test/path",
1977+
extensionUri: {} as vscode.Uri,
1978+
globalState: {
1979+
get: jest.fn(),
1980+
update: jest.fn(),
1981+
keys: jest.fn().mockReturnValue([]),
1982+
},
1983+
secrets: {
1984+
get: jest.fn(),
1985+
store: jest.fn(),
1986+
delete: jest.fn(),
1987+
},
1988+
subscriptions: [],
1989+
extension: {
1990+
packageJSON: { version: "1.0.0" },
1991+
},
1992+
globalStorageUri: {
1993+
fsPath: "/test/storage/path",
1994+
},
1995+
} as unknown as vscode.ExtensionContext
1996+
1997+
mockOutputChannel = {
1998+
appendLine: jest.fn(),
1999+
clear: jest.fn(),
2000+
dispose: jest.fn(),
2001+
} as unknown as vscode.OutputChannel
2002+
2003+
mockPostMessage = jest.fn()
2004+
mockWebviewView = {
2005+
webview: {
2006+
postMessage: mockPostMessage,
2007+
html: "",
2008+
options: {},
2009+
onDidReceiveMessage: jest.fn(),
2010+
asWebviewUri: jest.fn(),
2011+
},
2012+
visible: true,
2013+
onDidDispose: jest.fn(),
2014+
onDidChangeVisibility: jest.fn(),
2015+
} as unknown as vscode.WebviewView
2016+
2017+
provider = new ClineProvider(mockContext, mockOutputChannel)
2018+
})
2019+
2020+
test("handles openProjectMcpSettings message", async () => {
2021+
await provider.resolveWebviewView(mockWebviewView)
2022+
const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
2023+
2024+
// Mock workspace folders
2025+
;(vscode.workspace as any).workspaceFolders = [{ uri: { fsPath: "/test/workspace" } }]
2026+
2027+
// Mock fs functions
2028+
const fs = require("fs/promises")
2029+
fs.mkdir.mockResolvedValue(undefined)
2030+
fs.writeFile.mockResolvedValue(undefined)
2031+
2032+
// Trigger openProjectMcpSettings
2033+
await messageHandler({
2034+
type: "openProjectMcpSettings",
2035+
})
2036+
2037+
// Verify directory was created
2038+
expect(fs.mkdir).toHaveBeenCalledWith(
2039+
expect.stringContaining(".roo"),
2040+
expect.objectContaining({ recursive: true }),
2041+
)
2042+
2043+
// Verify file was created with default content
2044+
expect(fs.writeFile).toHaveBeenCalledWith(
2045+
expect.stringContaining("mcp.json"),
2046+
JSON.stringify({ mcpServers: {} }, null, 2),
2047+
)
2048+
})
2049+
2050+
test("handles openProjectMcpSettings when workspace is not open", async () => {
2051+
await provider.resolveWebviewView(mockWebviewView)
2052+
const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
2053+
2054+
// Mock no workspace folders
2055+
;(vscode.workspace as any).workspaceFolders = []
2056+
2057+
// Trigger openProjectMcpSettings
2058+
await messageHandler({
2059+
type: "openProjectMcpSettings",
2060+
})
2061+
2062+
// Verify error message was shown
2063+
expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("Please open a project folder first")
2064+
})
2065+
2066+
test("handles openProjectMcpSettings file creation error", async () => {
2067+
await provider.resolveWebviewView(mockWebviewView)
2068+
const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
2069+
2070+
// Mock workspace folders
2071+
;(vscode.workspace as any).workspaceFolders = [{ uri: { fsPath: "/test/workspace" } }]
2072+
2073+
// Mock fs functions to fail
2074+
const fs = require("fs/promises")
2075+
fs.mkdir.mockRejectedValue(new Error("Failed to create directory"))
2076+
2077+
// Trigger openProjectMcpSettings
2078+
await messageHandler({
2079+
type: "openProjectMcpSettings",
2080+
})
2081+
2082+
// Verify error message was shown
2083+
expect(vscode.window.showErrorMessage).toHaveBeenCalledWith(
2084+
expect.stringContaining("Failed to create or open .roo/mcp.json"),
2085+
)
2086+
})
2087+
})
2088+
19652089
describe("ContextProxy integration", () => {
19662090
let provider: ClineProvider
19672091
let mockContext: vscode.ExtensionContext

0 commit comments

Comments
 (0)