From 97b93c44fa0644c700004e800711d28286309d51 Mon Sep 17 00:00:00 2001 From: Dallin Romney Date: Sun, 9 Nov 2025 19:04:41 -0800 Subject: [PATCH 1/5] fix: validate config secrets present --- extensions/cli/src/services/MCPService.ts | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/extensions/cli/src/services/MCPService.ts b/extensions/cli/src/services/MCPService.ts index d261621c6ff..ead1658ea47 100644 --- a/extensions/cli/src/services/MCPService.ts +++ b/extensions/cli/src/services/MCPService.ts @@ -15,6 +15,7 @@ import { import { getErrorString } from "../util/error.js"; import { logger } from "../util/logger.js"; +import { TEMPLATE_VAR_REGEX } from "@continuedev/config-yaml"; import { BaseService, ServiceWithDependencies } from "./BaseService.js"; import { serviceContainer } from "./ServiceContainer.js"; import { @@ -249,6 +250,8 @@ export class MCPService this.updateState(); try { + this.validateConfigSecrets(serverConfig); + const client = await this.getConnectedClient(serverConfig); connection.client = client; @@ -352,6 +355,25 @@ export class MCPService this.updateState(); } + public validateConfigSecrets(serverConfig: MCPServerConfig): void { + const configString = JSON.stringify(serverConfig); + const unresolvedSecrets: string[] = []; + + let match; + const regex = new RegExp(TEMPLATE_VAR_REGEX); + while ((match = regex.exec(configString)) !== null) { + const fullPath = match[1]; + const secretName = fullPath.split(/[./]+/).pop() || fullPath; + unresolvedSecrets.push(secretName); + } + + if (unresolvedSecrets.length > 0) { + throw new Error( + `${serverConfig.name} MCP Server has unresolved secrets: ${unresolvedSecrets.join(", ")}`, + ); + } + } + /** * Shutdown a single connection */ From 2688fc9f6dd8ab7f964fd3fa9900279c7093e09e Mon Sep 17 00:00:00 2001 From: Dallin Romney Date: Sun, 9 Nov 2025 19:54:36 -0800 Subject: [PATCH 2/5] fix: CLI warnings/errors for unresolved MCP secrets --- core/config/yaml/loadYaml.ts | 17 --------- core/context/mcp/MCPConnection.ts | 17 +++++++++ extensions/cli/src/services/MCPService.ts | 42 +++++++++++------------ 3 files changed, 37 insertions(+), 39 deletions(-) diff --git a/core/config/yaml/loadYaml.ts b/core/config/yaml/loadYaml.ts index 44af239df88..40821bb04ca 100644 --- a/core/config/yaml/loadYaml.ts +++ b/core/config/yaml/loadYaml.ts @@ -9,7 +9,6 @@ import { ModelRole, PackageIdentifier, RegistryClient, - TEMPLATE_VAR_REGEX, unrollAssistant, validateConfigYaml, } from "@continuedev/config-yaml"; @@ -237,22 +236,6 @@ export async function configYamlToContinueConfig(options: { sourceFile: doc.sourceFile, })); - config.mcpServers?.forEach((mcpServer) => { - if ("args" in mcpServer) { - const mcpArgVariables = - mcpServer.args?.filter((arg) => TEMPLATE_VAR_REGEX.test(arg)) ?? []; - - if (mcpArgVariables.length === 0) { - return; - } - - localErrors.push({ - fatal: false, - message: `MCP server "${mcpServer.name}" has unsubstituted variables in args: ${mcpArgVariables.join(", ")}. Please refer to https://docs.continue.dev/hub/secrets/secret-types for managing hub secrets.`, - }); - } - }); - // Prompt files - try { const promptFiles = await getAllPromptFiles(ide, undefined, true); diff --git a/core/context/mcp/MCPConnection.ts b/core/context/mcp/MCPConnection.ts index 4090e92c581..80fea95bb70 100644 --- a/core/context/mcp/MCPConnection.ts +++ b/core/context/mcp/MCPConnection.ts @@ -2,6 +2,10 @@ import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { Transport } from "@modelcontextprotocol/sdk/shared/transport.js"; import { fileURLToPath } from "url"; +import { + decodeSecretLocation, + getTemplateVariables, +} from "@continuedev/config-yaml"; import { SSEClientTransport, SseError, @@ -163,6 +167,19 @@ class MCPConnection { } } + const vars = getTemplateVariables(JSON.stringify(this.options)); + const unrendered = vars.map((v) => { + return decodeSecretLocation(v.replace("secrets.", "")).secretName; + }); + + if (unrendered.length > 0) { + this.errors.push( + `${this.options.name} MCP Server has unresolved secrets: ${unrendered.join(", ")}. +For personal use you can set the secret in the hub at https://hub.continue.dev/settings/secrets. +Org-level secrets can only be used for MCP by Background Agents (https://docs.continue.dev/hub/agents/overview) when \"Include in Env\" is enabled.`, + ); + } + this.connectionPromise = Promise.race([ // If aborted by a refresh or other, cancel and don't do anything new Promise((resolve) => { diff --git a/extensions/cli/src/services/MCPService.ts b/extensions/cli/src/services/MCPService.ts index ead1658ea47..d87926364c3 100644 --- a/extensions/cli/src/services/MCPService.ts +++ b/extensions/cli/src/services/MCPService.ts @@ -15,7 +15,10 @@ import { import { getErrorString } from "../util/error.js"; import { logger } from "../util/logger.js"; -import { TEMPLATE_VAR_REGEX } from "@continuedev/config-yaml"; +import { + decodeSecretLocation, + getTemplateVariables, +} from "@continuedev/config-yaml"; import { BaseService, ServiceWithDependencies } from "./BaseService.js"; import { serviceContainer } from "./ServiceContainer.js"; import { @@ -249,9 +252,23 @@ export class MCPService this.connections.set(serverName, connection); this.updateState(); - try { - this.validateConfigSecrets(serverConfig); + const vars = getTemplateVariables(JSON.stringify(serverConfig)); + const unrendered = vars.map((v) => { + return decodeSecretLocation(v.replace("secrets.", "")).secretName; + }); + + if (unrendered.length > 0) { + const message = `${serverConfig.name} MCP Server has unresolved secrets: ${unrendered.join(", ")} +For personal use you can set the secret in the hub at https://hub.continue.dev/settings/secrets or pass it to the CLI environment. +Org-level secrets can only be used for MCP by Background Agents (https://docs.continue.dev/hub/agents/overview) when \"Include in Env\" is enabled for the secret.`; + if (this.isHeadless) { + throw new Error(message); + } else { + connection.warnings.push(message); + } + } + try { const client = await this.getConnectedClient(serverConfig); connection.client = client; @@ -355,25 +372,6 @@ export class MCPService this.updateState(); } - public validateConfigSecrets(serverConfig: MCPServerConfig): void { - const configString = JSON.stringify(serverConfig); - const unresolvedSecrets: string[] = []; - - let match; - const regex = new RegExp(TEMPLATE_VAR_REGEX); - while ((match = regex.exec(configString)) !== null) { - const fullPath = match[1]; - const secretName = fullPath.split(/[./]+/).pop() || fullPath; - unresolvedSecrets.push(secretName); - } - - if (unresolvedSecrets.length > 0) { - throw new Error( - `${serverConfig.name} MCP Server has unresolved secrets: ${unresolvedSecrets.join(", ")}`, - ); - } - } - /** * Shutdown a single connection */ From 86ed8e964dfd37e5298b0072b78fffc14d0b2fe8 Mon Sep 17 00:00:00 2001 From: Dallin Romney Date: Sun, 9 Nov 2025 19:56:07 -0800 Subject: [PATCH 3/5] fix: allow unrendered secrets error to set mcp status to error --- extensions/cli/src/services/MCPService.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/extensions/cli/src/services/MCPService.ts b/extensions/cli/src/services/MCPService.ts index d87926364c3..5ab03010b21 100644 --- a/extensions/cli/src/services/MCPService.ts +++ b/extensions/cli/src/services/MCPService.ts @@ -257,18 +257,18 @@ export class MCPService return decodeSecretLocation(v.replace("secrets.", "")).secretName; }); - if (unrendered.length > 0) { - const message = `${serverConfig.name} MCP Server has unresolved secrets: ${unrendered.join(", ")} + try { + if (unrendered.length > 0) { + const message = `${serverConfig.name} MCP Server has unresolved secrets: ${unrendered.join(", ")} For personal use you can set the secret in the hub at https://hub.continue.dev/settings/secrets or pass it to the CLI environment. Org-level secrets can only be used for MCP by Background Agents (https://docs.continue.dev/hub/agents/overview) when \"Include in Env\" is enabled for the secret.`; - if (this.isHeadless) { - throw new Error(message); - } else { - connection.warnings.push(message); + if (this.isHeadless) { + throw new Error(message); + } else { + connection.warnings.push(message); + } } - } - try { const client = await this.getConnectedClient(serverConfig); connection.client = client; From c7210d1f454b8856d28be64775ef428b9b1a988e Mon Sep 17 00:00:00 2001 From: Dallin Romney Date: Mon, 17 Nov 2025 15:01:11 -0800 Subject: [PATCH 4/5] fix: lint --- extensions/cli/src/services/MCPService.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/extensions/cli/src/services/MCPService.ts b/extensions/cli/src/services/MCPService.ts index e2a7a6c76d7..acf4a764a6e 100644 --- a/extensions/cli/src/services/MCPService.ts +++ b/extensions/cli/src/services/MCPService.ts @@ -1,3 +1,7 @@ +import { + decodeSecretLocation, + getTemplateVariables, +} from "@continuedev/config-yaml"; import { type AssistantConfig } from "@continuedev/sdk"; import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { @@ -15,10 +19,6 @@ import { import { getErrorString } from "../util/error.js"; import { logger } from "../util/logger.js"; -import { - decodeSecretLocation, - getTemplateVariables, -} from "@continuedev/config-yaml"; import { BaseService, ServiceWithDependencies } from "./BaseService.js"; import { serviceContainer } from "./ServiceContainer.js"; import { From 90f52a3b0e0f89db2fca69f3c9118af3ccb1e8f1 Mon Sep 17 00:00:00 2001 From: Dallin Romney Date: Mon, 17 Nov 2025 15:03:22 -0800 Subject: [PATCH 5/5] fix: another tweak --- core/context/mcp/MCPConnection.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/core/context/mcp/MCPConnection.ts b/core/context/mcp/MCPConnection.ts index 80fea95bb70..88d70970377 100644 --- a/core/context/mcp/MCPConnection.ts +++ b/core/context/mcp/MCPConnection.ts @@ -169,7 +169,12 @@ class MCPConnection { const vars = getTemplateVariables(JSON.stringify(this.options)); const unrendered = vars.map((v) => { - return decodeSecretLocation(v.replace("secrets.", "")).secretName; + const stripped = v.replace("secrets.", ""); + try { + return decodeSecretLocation(stripped).secretName; + } catch { + return stripped; + } }); if (unrendered.length > 0) {