Skip to content

Commit 05aebec

Browse files
committed
feat: add support for runtime MCP servers via IPC API (#7518)
- Add initializeRuntimeMcpServers method to McpHub for programmatic server initialization - Update API.startNewTask to detect and initialize MCP servers from configuration - Add "runtime" as a new server source type alongside "global" and "project" - Update type definitions to include mcpServers in GlobalSettings - Add comprehensive tests for the new functionality This allows MCP servers to be defined and initialized through the IPC socket API configuration rather than only through file-based configuration, enabling headless VSCode environments to use MCP servers programmatically. Fixes #7518
1 parent 1d46bd1 commit 05aebec

File tree

5 files changed

+308
-17
lines changed

5 files changed

+308
-17
lines changed

packages/types/src/global-settings.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@ export const globalSettingsSchema = z.object({
133133

134134
mcpEnabled: z.boolean().optional(),
135135
enableMcpServerCreation: z.boolean().optional(),
136+
mcpServers: z.record(z.string(), z.any()).optional(),
136137

137138
remoteControlEnabled: z.boolean().optional(),
138139

Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"
2+
import * as vscode from "vscode"
3+
import { API } from "../api"
4+
import { ClineProvider } from "../../core/webview/ClineProvider"
5+
import { Package } from "../../shared/package"
6+
7+
// Mock vscode module
8+
vi.mock("vscode", () => ({
9+
commands: {
10+
executeCommand: vi.fn(),
11+
},
12+
workspace: {
13+
getConfiguration: vi.fn(() => ({
14+
update: vi.fn(),
15+
})),
16+
},
17+
window: {
18+
showErrorMessage: vi.fn(),
19+
showInformationMessage: vi.fn(),
20+
showWarningMessage: vi.fn(),
21+
createTextEditorDecorationType: vi.fn().mockReturnValue({
22+
dispose: vi.fn(),
23+
}),
24+
},
25+
ConfigurationTarget: {
26+
Global: 1,
27+
},
28+
}))
29+
30+
describe("API", () => {
31+
let api: API
32+
let mockOutputChannel: any
33+
let mockProvider: any
34+
let mockMcpHub: any
35+
36+
beforeEach(() => {
37+
// Create mock output channel
38+
mockOutputChannel = {
39+
appendLine: vi.fn(),
40+
}
41+
42+
// Create mock MCP hub
43+
mockMcpHub = {
44+
initializeRuntimeMcpServers: vi.fn(),
45+
}
46+
47+
// Create mock provider
48+
mockProvider = {
49+
context: {
50+
extension: {
51+
packageJSON: {
52+
version: "1.0.0",
53+
},
54+
},
55+
},
56+
setValues: vi.fn(),
57+
removeClineFromStack: vi.fn(),
58+
postStateToWebview: vi.fn(),
59+
postMessageToWebview: vi.fn(),
60+
createTask: vi.fn().mockResolvedValue({ taskId: "test-task-id" }),
61+
getMcpHub: vi.fn().mockReturnValue(mockMcpHub),
62+
on: vi.fn(),
63+
off: vi.fn(),
64+
}
65+
66+
// Create API instance
67+
api = new API(mockOutputChannel, mockProvider as any)
68+
})
69+
70+
afterEach(() => {
71+
vi.clearAllMocks()
72+
})
73+
74+
describe("startNewTask", () => {
75+
it("should initialize runtime MCP servers when mcpServers is provided in configuration", async () => {
76+
const configuration = {
77+
apiProvider: "openai" as const,
78+
mcpServers: {
79+
"test-server": {
80+
type: "stdio",
81+
command: "node",
82+
args: ["test.js"],
83+
},
84+
},
85+
}
86+
87+
const taskId = await api.startNewTask({
88+
configuration,
89+
text: "Test task",
90+
})
91+
92+
// Verify MCP hub was retrieved
93+
expect(mockProvider.getMcpHub).toHaveBeenCalled()
94+
95+
// Verify runtime MCP servers were initialized
96+
expect(mockMcpHub.initializeRuntimeMcpServers).toHaveBeenCalledWith(configuration.mcpServers)
97+
98+
// Verify other methods were called
99+
expect(mockProvider.setValues).toHaveBeenCalledWith(configuration)
100+
expect(mockProvider.createTask).toHaveBeenCalled()
101+
expect(taskId).toBe("test-task-id")
102+
})
103+
104+
it("should not initialize MCP servers when mcpServers is not provided", async () => {
105+
const configuration = {
106+
apiProvider: "openai" as const,
107+
}
108+
109+
await api.startNewTask({
110+
configuration,
111+
text: "Test task",
112+
})
113+
114+
// Verify MCP hub was not retrieved
115+
expect(mockProvider.getMcpHub).not.toHaveBeenCalled()
116+
117+
// Verify runtime MCP servers were not initialized
118+
expect(mockMcpHub.initializeRuntimeMcpServers).not.toHaveBeenCalled()
119+
120+
// Verify other methods were still called
121+
expect(mockProvider.setValues).toHaveBeenCalledWith(configuration)
122+
expect(mockProvider.createTask).toHaveBeenCalled()
123+
})
124+
125+
it("should handle when MCP hub is not available", async () => {
126+
// Make getMcpHub return undefined
127+
mockProvider.getMcpHub.mockReturnValue(undefined)
128+
129+
const configuration = {
130+
apiProvider: "openai" as const,
131+
mcpServers: {
132+
"test-server": {
133+
type: "stdio",
134+
command: "node",
135+
args: ["test.js"],
136+
},
137+
},
138+
}
139+
140+
// Should not throw an error
141+
const taskId = await api.startNewTask({
142+
configuration,
143+
text: "Test task",
144+
})
145+
146+
// Verify MCP hub was retrieved
147+
expect(mockProvider.getMcpHub).toHaveBeenCalled()
148+
149+
// Verify runtime MCP servers were not initialized (since hub is undefined)
150+
expect(mockMcpHub.initializeRuntimeMcpServers).not.toHaveBeenCalled()
151+
152+
// Verify other methods were still called
153+
expect(mockProvider.setValues).toHaveBeenCalledWith(configuration)
154+
expect(mockProvider.createTask).toHaveBeenCalled()
155+
expect(taskId).toBe("test-task-id")
156+
})
157+
158+
it("should handle complex MCP server configurations", async () => {
159+
const configuration = {
160+
apiProvider: "openai" as const,
161+
mcpServers: {
162+
"stdio-server": {
163+
type: "stdio",
164+
command: "node",
165+
args: ["server.js"],
166+
env: { NODE_ENV: "production" },
167+
disabled: false,
168+
alwaysAllow: ["tool1", "tool2"],
169+
},
170+
"sse-server": {
171+
type: "sse",
172+
url: "http://localhost:8080/sse",
173+
headers: { Authorization: "Bearer token" },
174+
disabled: true,
175+
},
176+
},
177+
}
178+
179+
await api.startNewTask({
180+
configuration,
181+
text: "Test task",
182+
})
183+
184+
// Verify runtime MCP servers were initialized with the full configuration
185+
expect(mockMcpHub.initializeRuntimeMcpServers).toHaveBeenCalledWith(configuration.mcpServers)
186+
})
187+
188+
it("should handle empty mcpServers object", async () => {
189+
const configuration = {
190+
apiProvider: "openai" as const,
191+
mcpServers: {},
192+
}
193+
194+
await api.startNewTask({
195+
configuration,
196+
text: "Test task",
197+
})
198+
199+
// Verify MCP hub was retrieved
200+
expect(mockProvider.getMcpHub).toHaveBeenCalled()
201+
202+
// Verify runtime MCP servers were initialized with empty object
203+
expect(mockMcpHub.initializeRuntimeMcpServers).toHaveBeenCalledWith({})
204+
})
205+
})
206+
})

src/extension/api.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,15 @@ export class API extends EventEmitter<RooCodeEvents> implements RooCodeAPI {
129129
}
130130

131131
if (configuration) {
132+
// Handle MCP servers if provided in configuration
133+
if (configuration.mcpServers && typeof configuration.mcpServers === "object") {
134+
const mcpHub = provider.getMcpHub()
135+
if (mcpHub) {
136+
// Initialize runtime MCP servers
137+
await mcpHub.initializeRuntimeMcpServers(configuration.mcpServers)
138+
}
139+
}
140+
132141
await provider.setValues(configuration)
133142

134143
if (configuration.allowedCommands) {

0 commit comments

Comments
 (0)