From a37a04633dc87338cb7bc286d08f06ae8fe2e902 Mon Sep 17 00:00:00 2001 From: Rod Boev Date: Thu, 11 Jun 2026 16:21:34 -0400 Subject: [PATCH] fix(mcp): keep global TLS opt-out from disabling MCP verification (#11468) --- core/config/load.ts | 3 +- core/config/yaml/loadYaml.vitest.ts | 104 +++++++++++++++++++++++ core/config/yaml/yamlToContinueConfig.ts | 27 +++++- 3 files changed, 132 insertions(+), 2 deletions(-) diff --git a/core/config/load.ts b/core/config/load.ts index b85150752a2..53ee9b7cf9a 100644 --- a/core/config/load.ts +++ b/core/config/load.ts @@ -70,6 +70,7 @@ import { serializePromptTemplates, } from "./util"; import { validateConfig } from "./validation.js"; +import { mergeMcpRequestOptions } from "./yaml/yamlToContinueConfig"; export function resolveSerializedConfig( filepath: string, @@ -526,7 +527,7 @@ async function intermediateToFinalConfig({ ).map((server, index) => ({ id: `continue-mcp-server-${index + 1}`, name: `MCP Server`, - requestOptions: mergeConfigYamlRequestOptions( + requestOptions: mergeMcpRequestOptions( server.transport.type !== "stdio" ? server.transport.requestOptions : undefined, diff --git a/core/config/yaml/loadYaml.vitest.ts b/core/config/yaml/loadYaml.vitest.ts index 97c2914437c..e74334eca29 100644 --- a/core/config/yaml/loadYaml.vitest.ts +++ b/core/config/yaml/loadYaml.vitest.ts @@ -3,6 +3,7 @@ import { validateConfigYaml, } from "@continuedev/config-yaml"; import { describe, expect, it } from "vitest"; +import { convertYamlMcpConfigToInternalMcpOptions } from "./yamlToContinueConfig"; describe("MCP Server cwd configuration", () => { describe("YAML schema validation", () => { @@ -126,3 +127,106 @@ describe("MCP Server cwd configuration", () => { }); }); }); + +describe("convertYamlMcpConfigToInternalMcpOptions", () => { + it("does not inherit global verifySsl false into remote MCP request options", () => { + const result = convertYamlMcpConfigToInternalMcpOptions( + { + name: "remote", + type: "sse", + url: "https://mcp.example.com", + }, + { + verifySsl: false, + headers: { + "X-Test": "1", + }, + proxy: "https://proxy.example.com", + }, + ); + + expect("requestOptions" in result).toBe(true); + expect(result.requestOptions).toEqual({ + headers: { + "X-Test": "1", + }, + proxy: "https://proxy.example.com", + }); + expect(result.requestOptions?.verifySsl).toBeUndefined(); + expect(result.requestOptions).not.toHaveProperty("verifySsl"); + }); + + it("does not inherit global verifySsl false into streamable HTTP MCP options", () => { + const result = convertYamlMcpConfigToInternalMcpOptions( + { + name: "remote", + type: "streamable-http", + url: "https://mcp.example.com", + }, + { + verifySsl: false, + timeout: 30, + }, + ); + + expect("requestOptions" in result).toBe(true); + expect(result.requestOptions).toEqual({ + timeout: 30, + }); + expect(result.requestOptions).not.toHaveProperty("verifySsl"); + }); + + it("preserves explicit per-server verifySsl false", () => { + const result = convertYamlMcpConfigToInternalMcpOptions( + { + name: "remote", + type: "sse", + url: "https://mcp.example.com", + requestOptions: { + verifySsl: false, + }, + }, + { + verifySsl: false, + }, + ); + + expect("requestOptions" in result).toBe(true); + expect(result.requestOptions?.verifySsl).toBe(false); + }); + + it("preserves explicit per-server verifySsl true", () => { + const result = convertYamlMcpConfigToInternalMcpOptions( + { + name: "remote", + type: "streamable-http", + url: "https://mcp.example.com", + requestOptions: { + verifySsl: true, + }, + }, + { + verifySsl: false, + }, + ); + + expect("requestOptions" in result).toBe(true); + expect(result.requestOptions?.verifySsl).toBe(true); + }); + + it("returns no request options when only global verifySsl is set", () => { + const result = convertYamlMcpConfigToInternalMcpOptions( + { + name: "remote", + type: "sse", + url: "https://mcp.example.com", + }, + { + verifySsl: false, + }, + ); + + expect("requestOptions" in result).toBe(true); + expect(result.requestOptions).toBeUndefined(); + }); +}); diff --git a/core/config/yaml/yamlToContinueConfig.ts b/core/config/yaml/yamlToContinueConfig.ts index d639e451554..8efe609b5f3 100644 --- a/core/config/yaml/yamlToContinueConfig.ts +++ b/core/config/yaml/yamlToContinueConfig.ts @@ -32,6 +32,31 @@ export function convertYamlRuleToContinueRule(rule: Rule): RuleWithSource { } } +export function mergeMcpRequestOptions( + requestOptions?: RequestOptions, + globalRequestOptions?: RequestOptions, +): RequestOptions | undefined { + if (!globalRequestOptions) { + return requestOptions; + } + + const { + verifySsl: _globalVerifySsl, + ...globalRequestOptionsWithoutVerifySsl + } = globalRequestOptions; + + // Global verifySsl can disable MCP certificate validation without a server-specific opt-out. + const sanitizedGlobalRequestOptions = + Object.keys(globalRequestOptionsWithoutVerifySsl).length > 0 + ? globalRequestOptionsWithoutVerifySsl + : undefined; + + return mergeConfigYamlRequestOptions( + requestOptions, + sanitizedGlobalRequestOptions, + ); +} + export function convertYamlMcpConfigToInternalMcpOptions( config: MCPServer, globalRequestOptions?: RequestOptions, @@ -66,7 +91,7 @@ export function convertYamlMcpConfigToInternalMcpOptions( type, url, apiKey, - requestOptions: mergeConfigYamlRequestOptions( + requestOptions: mergeMcpRequestOptions( requestOptions, globalRequestOptions, ),