feat: add setting to enable/disable MCP server creation

- Add enableMcpServerCreation setting to control whether MCP server creation is allowed
- Add UI toggle in settings view for this feature
- Update system prompt to conditionally include MCP server creation documentation
- Add tests for new functionality
This commit is contained in:
sam hoang
2025-02-01 09:34:53 +07:00
parent 8ce5f9a890
commit f906755d90
12 changed files with 1263 additions and 12 deletions

View File

@@ -842,8 +842,14 @@ export class Cline {
})
}
const { browserViewportSize, mode, customModePrompts, preferredLanguage, experiments } =
(await this.providerRef.deref()?.getState()) ?? {}
const {
browserViewportSize,
mode,
customModePrompts,
preferredLanguage,
experiments,
enableMcpServerCreation,
} = (await this.providerRef.deref()?.getState()) ?? {}
const { customModes } = (await this.providerRef.deref()?.getState()) ?? {}
const systemPrompt = await (async () => {
const provider = this.providerRef.deref()
@@ -864,6 +870,7 @@ export class Cline {
preferredLanguage,
this.diffEnabled,
experiments,
enableMcpServerCreation,
)
})()

File diff suppressed because it is too large Load Diff

View File

@@ -174,6 +174,7 @@ describe("SYSTEM_PROMPT", () => {
undefined, // preferredLanguage
undefined, // diffEnabled
experiments,
true, // enableMcpServerCreation
)
expect(prompt).toMatchSnapshot()
@@ -194,6 +195,7 @@ describe("SYSTEM_PROMPT", () => {
undefined, // preferredLanguage
undefined, // diffEnabled
experiments,
true, // enableMcpServerCreation
)
expect(prompt).toMatchSnapshot()
@@ -216,6 +218,7 @@ describe("SYSTEM_PROMPT", () => {
undefined, // preferredLanguage
undefined, // diffEnabled
experiments,
true, // enableMcpServerCreation
)
expect(prompt).toMatchSnapshot()
@@ -236,6 +239,7 @@ describe("SYSTEM_PROMPT", () => {
undefined, // preferredLanguage
undefined, // diffEnabled
experiments,
true, // enableMcpServerCreation
)
expect(prompt).toMatchSnapshot()
@@ -256,6 +260,7 @@ describe("SYSTEM_PROMPT", () => {
undefined, // preferredLanguage
undefined, // diffEnabled
experiments,
true, // enableMcpServerCreation
)
expect(prompt).toMatchSnapshot()
@@ -276,6 +281,7 @@ describe("SYSTEM_PROMPT", () => {
undefined, // preferredLanguage
true, // diffEnabled
experiments,
true, // enableMcpServerCreation
)
expect(prompt).toContain("apply_diff")
@@ -297,6 +303,7 @@ describe("SYSTEM_PROMPT", () => {
undefined, // preferredLanguage
false, // diffEnabled
experiments,
true, // enableMcpServerCreation
)
expect(prompt).not.toContain("apply_diff")
@@ -318,6 +325,7 @@ describe("SYSTEM_PROMPT", () => {
undefined, // preferredLanguage
undefined, // diffEnabled
experiments,
true, // enableMcpServerCreation
)
expect(prompt).not.toContain("apply_diff")
@@ -339,6 +347,7 @@ describe("SYSTEM_PROMPT", () => {
"Spanish", // preferredLanguage
undefined, // diffEnabled
experiments,
true, // enableMcpServerCreation
)
expect(prompt).toContain("Language Preference:")
@@ -371,6 +380,7 @@ describe("SYSTEM_PROMPT", () => {
undefined, // preferredLanguage
undefined, // diffEnabled
experiments,
true, // enableMcpServerCreation
)
// Role definition should be at the top
@@ -406,6 +416,7 @@ describe("SYSTEM_PROMPT", () => {
undefined,
undefined,
experiments,
true, // enableMcpServerCreation
)
// Role definition from promptComponent should be at the top
@@ -436,6 +447,7 @@ describe("SYSTEM_PROMPT", () => {
undefined,
undefined,
experiments,
true, // enableMcpServerCreation
)
// Should use the default mode's role definition
@@ -458,6 +470,7 @@ describe("SYSTEM_PROMPT", () => {
undefined, // preferredLanguage
undefined, // diffEnabled
experiments, // experiments - undefined should disable all experimental tools
true, // enableMcpServerCreation
)
// Verify experimental tools are not included in the prompt
@@ -485,6 +498,7 @@ describe("SYSTEM_PROMPT", () => {
undefined, // preferredLanguage
undefined, // diffEnabled
experiments,
true, // enableMcpServerCreation
)
// Verify experimental tools are included in the prompt when enabled
@@ -512,6 +526,7 @@ describe("SYSTEM_PROMPT", () => {
undefined, // preferredLanguage
undefined, // diffEnabled
experiments,
true, // enableMcpServerCreation
)
// Verify only enabled experimental tools are included
@@ -539,6 +554,7 @@ describe("SYSTEM_PROMPT", () => {
undefined,
true, // diffEnabled
experiments,
true, // enableMcpServerCreation
)
// Verify base instruction lists all available tools
@@ -568,6 +584,7 @@ describe("SYSTEM_PROMPT", () => {
undefined,
true,
experiments,
true, // enableMcpServerCreation
)
// Verify detailed instructions for each tool
@@ -623,6 +640,7 @@ describe("addCustomInstructions", () => {
undefined,
undefined,
experiments,
true, // enableMcpServerCreation
)
expect(prompt).toMatchSnapshot()
@@ -643,11 +661,60 @@ describe("addCustomInstructions", () => {
undefined,
undefined,
experiments,
true, // enableMcpServerCreation
)
expect(prompt).toMatchSnapshot()
})
it("should include MCP server creation info when enabled", async () => {
const mockMcpHub = createMockMcpHub()
const prompt = await SYSTEM_PROMPT(
mockContext,
"/test/path",
false, // supportsComputerUse
mockMcpHub, // mcpHub
undefined, // diffStrategy
undefined, // browserViewportSize
defaultModeSlug, // mode
undefined, // customModePrompts
undefined, // customModes,
undefined, // globalCustomInstructions
undefined, // preferredLanguage
undefined, // diffEnabled
experiments,
true, // enableMcpServerCreation
)
expect(prompt).toContain("Creating an MCP Server")
expect(prompt).toMatchSnapshot()
})
it("should exclude MCP server creation info when disabled", async () => {
const mockMcpHub = createMockMcpHub()
const prompt = await SYSTEM_PROMPT(
mockContext,
"/test/path",
false, // supportsComputerUse
mockMcpHub, // mcpHub
undefined, // diffStrategy
undefined, // browserViewportSize
defaultModeSlug, // mode
undefined, // customModePrompts
undefined, // customModes,
undefined, // globalCustomInstructions
undefined, // preferredLanguage
undefined, // diffEnabled
experiments,
false, // enableMcpServerCreation
)
expect(prompt).not.toContain("Creating an MCP Server")
expect(prompt).toMatchSnapshot()
})
it("should prioritize mode-specific rules for code mode", async () => {
const instructions = await addCustomInstructions("", "", "/test/path", defaultModeSlug)
expect(instructions).toMatchSnapshot()

View File

@@ -1,7 +1,11 @@
import { DiffStrategy } from "../../diff/DiffStrategy"
import { McpHub } from "../../../services/mcp/McpHub"
export async function getMcpServersSection(mcpHub?: McpHub, diffStrategy?: DiffStrategy): Promise<string> {
export async function getMcpServersSection(
mcpHub?: McpHub,
diffStrategy?: DiffStrategy,
enableMcpServerCreation?: boolean,
): Promise<string> {
if (!mcpHub) {
return ""
}
@@ -43,7 +47,7 @@ export async function getMcpServersSection(mcpHub?: McpHub, diffStrategy?: DiffS
.join("\n\n")}`
: "(No MCP servers currently connected)"
return `MCP SERVERS
const baseSection = `MCP SERVERS
The Model Context Protocol (MCP) enables communication between the system and locally running MCP servers that provide additional tools and resources to extend your capabilities.
@@ -51,7 +55,15 @@ The Model Context Protocol (MCP) enables communication between the system and lo
When a server is connected, you can use the server's tools via the \`use_mcp_tool\` tool, and access the server's resources via the \`access_mcp_resource\` tool.
${connectedServers}
${connectedServers}`
if (!enableMcpServerCreation) {
return baseSection
}
return (
baseSection +
`
## Creating an MCP Server
@@ -86,7 +98,7 @@ weather-server/
...
"type": "module", // added by default, uses ES module syntax (import/export) rather than CommonJS (require/module.exports) (Important to know if you create additional scripts in this server repository like a get-refresh-token.js script)
"scripts": {
"build": "tsc && node -e \"require('fs').chmodSync('build/index.js', '755')\"",
"build": "tsc && node -e "require('fs').chmodSync('build/index.js', '755')"",
...
}
...
@@ -398,11 +410,11 @@ IMPORTANT: Regardless of what else you see in the MCP settings file, you must de
## Editing MCP Servers
The user may ask to add tools or resources that may make sense to add to an existing MCP server (listed under 'Connected MCP Servers' above: ${
mcpHub
.getServers()
.map((server) => server.name)
.join(", ") || "(None running currently)"
}, e.g. if it would use the same API. This would be possible if you can locate the MCP server repository on the user's system by looking at the server arguments for a filepath. You might then use list_files and read_file to explore the files in the repository, and use write_to_file${diffStrategy ? " or apply_diff" : ""} to make changes to the files.
mcpHub
.getServers()
.map((server) => server.name)
.join(", ") || "(None running currently)"
}, e.g. if it would use the same API. This would be possible if you can locate the MCP server repository on the user's system by looking at the server arguments for a filepath. You might then use list_files and read_file to explore the files in the repository, and use write_to_file${diffStrategy ? " or apply_diff" : ""} to make changes to the files.
However some MCP servers may be running from installed packages rather than a local repository, in which case it may make more sense to create a new MCP server.
@@ -411,4 +423,5 @@ However some MCP servers may be running from installed packages rather than a lo
The user may not always request the use or creation of MCP servers. Instead, they might provide tasks that can be completed with existing tools. While using the MCP SDK to extend your capabilities can be useful, it's important to understand that this is just one specialized type of task you can accomplish. You should only implement MCP servers when the user explicitly requests it (e.g., "add a tool that...").
Remember: The MCP documentation and example provided above are to help you understand and work with existing MCP servers or create new ones when requested by the user. You already have access to tools and capabilities that can be used to accomplish a wide range of tasks.`
)
}

View File

@@ -40,6 +40,7 @@ async function generatePrompt(
preferredLanguage?: string,
diffEnabled?: boolean,
experiments?: Record<string, boolean>,
enableMcpServerCreation?: boolean,
): Promise<string> {
if (!context) {
throw new Error("Extension context is required for generating system prompt")
@@ -49,7 +50,7 @@ async function generatePrompt(
const effectiveDiffStrategy = diffEnabled ? diffStrategy : undefined
const [mcpServersSection, modesSection] = await Promise.all([
getMcpServersSection(mcpHub, effectiveDiffStrategy),
getMcpServersSection(mcpHub, effectiveDiffStrategy, enableMcpServerCreation),
getModesSection(context),
])
@@ -105,6 +106,7 @@ export const SYSTEM_PROMPT = async (
preferredLanguage?: string,
diffEnabled?: boolean,
experiments?: Record<string, boolean>,
enableMcpServerCreation?: boolean,
): Promise<string> => {
if (!context) {
throw new Error("Extension context is required for generating system prompt")
@@ -139,5 +141,6 @@ export const SYSTEM_PROMPT = async (
preferredLanguage,
diffEnabled,
experiments,
enableMcpServerCreation,
)
}

View File

@@ -102,6 +102,7 @@ type GlobalStateKey =
| "writeDelayMs"
| "terminalOutputLineLimit"
| "mcpEnabled"
| "enableMcpServerCreation"
| "alwaysApproveResubmit"
| "requestDelaySeconds"
| "rateLimitSeconds"
@@ -841,6 +842,10 @@ export class ClineProvider implements vscode.WebviewViewProvider {
await this.updateGlobalState("mcpEnabled", mcpEnabled)
await this.postStateToWebview()
break
case "enableMcpServerCreation":
await this.updateGlobalState("enableMcpServerCreation", message.bool ?? true)
await this.postStateToWebview()
break
case "playSound":
if (message.audioType) {
const soundPath = path.join(this.context.extensionPath, "audio", `${message.audioType}.wav`)
@@ -1129,6 +1134,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
mcpEnabled,
fuzzyMatchThreshold,
experiments,
enableMcpServerCreation,
} = await this.getState()
// Create diffStrategy based on current model and settings
@@ -1157,6 +1163,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
preferredLanguage,
diffEnabled,
experiments,
enableMcpServerCreation,
)
await this.postMessageToWebview({
@@ -1994,6 +2001,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
terminalOutputLineLimit,
fuzzyMatchThreshold,
mcpEnabled,
enableMcpServerCreation,
alwaysApproveResubmit,
requestDelaySeconds,
rateLimitSeconds,
@@ -2036,6 +2044,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
terminalOutputLineLimit: terminalOutputLineLimit ?? 500,
fuzzyMatchThreshold: fuzzyMatchThreshold ?? 1.0,
mcpEnabled: mcpEnabled ?? true,
enableMcpServerCreation: enableMcpServerCreation ?? true,
alwaysApproveResubmit: alwaysApproveResubmit ?? false,
requestDelaySeconds: requestDelaySeconds ?? 10,
rateLimitSeconds: rateLimitSeconds ?? 0,
@@ -2160,6 +2169,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
screenshotQuality,
terminalOutputLineLimit,
mcpEnabled,
enableMcpServerCreation,
alwaysApproveResubmit,
requestDelaySeconds,
rateLimitSeconds,
@@ -2233,6 +2243,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
this.getGlobalState("screenshotQuality") as Promise<number | undefined>,
this.getGlobalState("terminalOutputLineLimit") as Promise<number | undefined>,
this.getGlobalState("mcpEnabled") as Promise<boolean | undefined>,
this.getGlobalState("enableMcpServerCreation") as Promise<boolean | undefined>,
this.getGlobalState("alwaysApproveResubmit") as Promise<boolean | undefined>,
this.getGlobalState("requestDelaySeconds") as Promise<number | undefined>,
this.getGlobalState("rateLimitSeconds") as Promise<number | undefined>,
@@ -2356,6 +2367,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
return langMap[vscodeLang.split("-")[0]] ?? "English"
})(),
mcpEnabled: mcpEnabled ?? true,
enableMcpServerCreation: enableMcpServerCreation ?? true,
alwaysApproveResubmit: alwaysApproveResubmit ?? false,
requestDelaySeconds: Math.max(5, requestDelaySeconds ?? 10),
rateLimitSeconds: rateLimitSeconds ?? 0,

View File

@@ -323,6 +323,7 @@ describe("ClineProvider", () => {
browserViewportSize: "900x600",
fuzzyMatchThreshold: 1.0,
mcpEnabled: true,
enableMcpServerCreation: false,
requestDelaySeconds: 5,
rateLimitSeconds: 0,
mode: defaultModeSlug,
@@ -895,6 +896,7 @@ describe("ClineProvider", () => {
},
},
mcpEnabled: true,
enableMcpServerCreation: false,
mode: "code" as const,
experiments: experimentDefault,
} as any)
@@ -927,6 +929,7 @@ describe("ClineProvider", () => {
},
},
mcpEnabled: false,
enableMcpServerCreation: false,
mode: "code" as const,
experiments: experimentDefault,
} as any)
@@ -991,6 +994,7 @@ describe("ClineProvider", () => {
},
customModePrompts: {},
mode: "code",
enableMcpServerCreation: true,
mcpEnabled: false,
browserViewportSize: "900x600",
experimentalDiffStrategy: true,
@@ -1025,6 +1029,7 @@ describe("ClineProvider", () => {
undefined, // preferredLanguage
true, // diffEnabled
experimentDefault,
true,
)
// Run the test again to verify it's consistent
@@ -1048,6 +1053,7 @@ describe("ClineProvider", () => {
diffEnabled: false,
fuzzyMatchThreshold: 0.8,
experiments: experimentDefault,
enableMcpServerCreation: true,
} as any)
// Mock SYSTEM_PROMPT to verify diffEnabled is passed as false
@@ -1076,6 +1082,7 @@ describe("ClineProvider", () => {
undefined, // preferredLanguage
false, // diffEnabled
experimentDefault,
true,
)
})
@@ -1090,6 +1097,7 @@ describe("ClineProvider", () => {
architect: { customInstructions: "Architect mode instructions" },
},
mode: "architect",
enableMcpServerCreation: false,
mcpEnabled: false,
browserViewportSize: "900x600",
experiments: experimentDefault,

View File

@@ -107,6 +107,7 @@ export interface ExtensionState {
writeDelayMs: number
terminalOutputLineLimit?: number
mcpEnabled: boolean
enableMcpServerCreation: boolean
mode: Mode
modeApiConfigs?: Record<Mode, string>
enhancementApiConfigId?: string

View File

@@ -62,6 +62,7 @@ export interface WebviewMessage {
| "deleteMessage"
| "terminalOutputLineLimit"
| "mcpEnabled"
| "enableMcpServerCreation"
| "searchCommits"
| "refreshGlamaModels"
| "alwaysApproveResubmit"

View File

@@ -45,6 +45,7 @@ describe("AutoApproveMenu", () => {
filePaths: [],
experiments: experimentDefault,
customModes: [],
enableMcpServerCreation: false,
// Auto-approve specific properties
alwaysAllowReadOnly: false,
@@ -91,6 +92,7 @@ describe("AutoApproveMenu", () => {
setExperimentEnabled: jest.fn(),
handleInputChange: jest.fn(),
setCustomModes: jest.fn(),
setEnableMcpServerCreation: jest.fn(),
}
beforeEach(() => {

View File

@@ -61,6 +61,8 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
setExperimentEnabled,
alwaysAllowModeSwitch,
setAlwaysAllowModeSwitch,
enableMcpServerCreation,
setEnableMcpServerCreation,
} = useExtensionState()
const [apiErrorMessage, setApiErrorMessage] = useState<string | undefined>(undefined)
const [modelIdErrorMessage, setModelIdErrorMessage] = useState<string | undefined>(undefined)
@@ -108,6 +110,7 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
})
vscode.postMessage({ type: "alwaysAllowModeSwitch", bool: alwaysAllowModeSwitch })
vscode.postMessage({ type: "enableMcpServerCreation", bool: enableMcpServerCreation })
onDone()
}
}
@@ -357,6 +360,17 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
</p>
</div>
<div style={{ marginBottom: 5 }}>
<VSCodeCheckbox
checked={enableMcpServerCreation}
onChange={(e: any) => setEnableMcpServerCreation(e.target.checked)}>
<span style={{ fontWeight: "500" }}>Enable MCP Server Creation</span>
</VSCodeCheckbox>
<p style={{ fontSize: "12px", marginTop: "5px", color: "var(--vscode-descriptionForeground)" }}>
This option allow user to enable and disable MCP server creation for saved tokens usage
</p>
</div>
<div style={{ marginBottom: 15 }}>
<VSCodeCheckbox
checked={alwaysAllowModeSwitch}

View File

@@ -51,6 +51,8 @@ export interface ExtensionStateContextType extends ExtensionState {
setTerminalOutputLineLimit: (value: number) => void
mcpEnabled: boolean
setMcpEnabled: (value: boolean) => void
enableMcpServerCreation: boolean
setEnableMcpServerCreation: (value: boolean) => void
alwaysApproveResubmit?: boolean
setAlwaysApproveResubmit: (value: boolean) => void
requestDelaySeconds: number
@@ -92,6 +94,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
screenshotQuality: 75,
terminalOutputLineLimit: 500,
mcpEnabled: true,
enableMcpServerCreation: true,
alwaysApproveResubmit: false,
requestDelaySeconds: 5,
rateLimitSeconds: 0, // Minimum time between successive requests (0 = disabled)
@@ -272,6 +275,8 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
setTerminalOutputLineLimit: (value) =>
setState((prevState) => ({ ...prevState, terminalOutputLineLimit: value })),
setMcpEnabled: (value) => setState((prevState) => ({ ...prevState, mcpEnabled: value })),
setEnableMcpServerCreation: (value) =>
setState((prevState) => ({ ...prevState, enableMcpServerCreation: value })),
setAlwaysApproveResubmit: (value) => setState((prevState) => ({ ...prevState, alwaysApproveResubmit: value })),
setRequestDelaySeconds: (value) => setState((prevState) => ({ ...prevState, requestDelaySeconds: value })),
setRateLimitSeconds: (value) => setState((prevState) => ({ ...prevState, rateLimitSeconds: value })),