From 42ca0fee6808222b0e6d284809e6a0a286d587b2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 15 Aug 2026 05:49:27 +0000 Subject: [PATCH 1/4] Initial plan From 182ccda8c06bcb530ee3a97630f910ef45e21a6e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 15 Aug 2026 05:58:51 +0000 Subject: [PATCH 2/4] Fail on omitted required MCP servers Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/start_mcp_gateway.cjs | 108 ++++++++++++++++++++ actions/setup/js/start_mcp_gateway.test.cjs | 87 ++++++++++++++++ 2 files changed, 195 insertions(+) diff --git a/actions/setup/js/start_mcp_gateway.cjs b/actions/setup/js/start_mcp_gateway.cjs index 17a79ad45e8..5b894e34533 100644 --- a/actions/setup/js/start_mcp_gateway.cjs +++ b/actions/setup/js/start_mcp_gateway.cjs @@ -269,6 +269,99 @@ function extractOptionalServerNames(configObj) { return optional; } +/** + * @param {unknown} serverConfig + * @param {NodeJS.ProcessEnv} env + * @returns {string[]} + */ +function getMissingEnvVarNamesForServer(serverConfig, env = process.env) { + if (!serverConfig || typeof serverConfig !== "object" || Array.isArray(serverConfig)) { + return []; + } + const server = /** @type {Record} */ serverConfig; + const serverEnv = server["env"]; + if (!serverEnv || typeof serverEnv !== "object" || Array.isArray(serverEnv)) { + return []; + } + + const missing = new Set(); + const varPattern = /\$\{([A-Z_][A-Z0-9_]*)\}/g; + for (const [envName, rawValue] of Object.entries(/** @type {Record} */ serverEnv)) { + if (typeof rawValue !== "string") { + continue; + } + const matches = [...rawValue.matchAll(varPattern)]; + if (matches.length === 0) { + if (rawValue.trim() === "") { + missing.add(envName); + } + continue; + } + for (const match of matches) { + const varName = match[1]; + if (!env[varName] || env[varName].trim() === "") { + missing.add(varName); + } + } + } + return Array.from(missing).sort(); +} + +/** + * Find required MCP servers that were present in the input configuration but are + * absent from the gateway output. Missing required servers usually indicate that + * startup-time configuration such as secrets or environment variables was empty. + * + * @param {Record} inputConfig + * @param {Record} gatewayOutput + * @param {string[]} [optionalServerNames] + * @param {NodeJS.ProcessEnv} [env] + * @returns {Array<{name: string, missingEnvVars: string[]}>} + */ +function findOmittedRequiredMCPServers(inputConfig, gatewayOutput, optionalServerNames = [], env = process.env) { + const inputServers = inputConfig && inputConfig.mcpServers; + if (!inputServers || typeof inputServers !== "object" || Array.isArray(inputServers)) { + return []; + } + const outputServers = gatewayOutput && gatewayOutput.mcpServers; + const outputServerMap = outputServers && typeof outputServers === "object" && !Array.isArray(outputServers) ? /** @type {Record} */ outputServers : {}; + const optional = new Set(optionalServerNames); + + return Object.entries(/** @type {Record} */ inputServers) + .filter(([name, serverConfig]) => { + if (optional.has(name)) { + return false; + } + if (serverConfig && typeof serverConfig === "object" && !Array.isArray(serverConfig)) { + const server = /** @type {Record} */ serverConfig; + if (server.required === false) { + return false; + } + } + return !Object.prototype.hasOwnProperty.call(outputServerMap, name); + }) + .map(([name, serverConfig]) => ({ + name, + missingEnvVars: getMissingEnvVarNamesForServer(serverConfig, env), + })); +} + +/** + * @param {Array<{name: string, missingEnvVars: string[]}>} omittedServers + * @returns {string} + */ +function formatOmittedRequiredMCPServersMessage(omittedServers) { + const details = omittedServers + .map(server => { + if (server.missingEnvVars.length === 0) { + return server.name; + } + return `${server.name} (missing/empty env: ${server.missingEnvVars.join(", ")})`; + }) + .join("; "); + return `Required MCP server(s) were omitted from the gateway output: ${details}. ` + `Configure the missing secrets/environment variables, or mark intentionally best-effort servers with required: false.`; +} + /** * Check whether a process is alive. * @param {number} pid @@ -878,6 +971,18 @@ async function main() { core.setFailed("ERROR: Gateway returned an error payload instead of configuration"); return; } + const omittedRequiredServers = findOmittedRequiredMCPServers(configObj, gatewayOutput, optionalServerNames); + if (omittedRequiredServers.length > 0) { + const message = formatOmittedRequiredMCPServersMessage(omittedRequiredServers); + core.error(`ERROR: ${message}`); + try { + process.kill(gatewayPid); + } catch { + // ignore + } + core.setFailed(`ERROR: ${message}`); + return; + } // ----------------------------------------------------------------------- // Convert gateway output to agent-specific format @@ -1100,9 +1205,12 @@ module.exports = { applyOTLPIgnoreIfMissing, detectEngineType, extractOptionalServerNames, + findOmittedRequiredMCPServers, + formatOmittedRequiredMCPServersMessage, getOTLPIfMissingMode, hasNonEmptyOTLPHeaders, isOTLPIfMissingIgnore, + getMissingEnvVarNamesForServer, getJSONParseErrorContext, injectCustomGatewayEnvArgs, normalizeSinkVisibilityEncoding, diff --git a/actions/setup/js/start_mcp_gateway.test.cjs b/actions/setup/js/start_mcp_gateway.test.cjs index a863a243960..ed773ac8b23 100644 --- a/actions/setup/js/start_mcp_gateway.test.cjs +++ b/actions/setup/js/start_mcp_gateway.test.cjs @@ -4,7 +4,10 @@ import { applyOTLPIgnoreIfMissing, detectEngineType, extractOptionalServerNames, + findOmittedRequiredMCPServers, + formatOmittedRequiredMCPServersMessage, getJSONParseErrorContext, + getMissingEnvVarNamesForServer, getOTLPIfMissingMode, hasNonEmptyOTLPHeaders, injectCustomGatewayEnvArgs, @@ -352,3 +355,87 @@ describe("start_mcp_gateway extractOptionalServerNames", () => { expect(extractOptionalServerNames({ mcpServers: null })).toEqual([]); }); }); + +describe("start_mcp_gateway omitted required MCP server detection", () => { + it("reports configured required servers missing from gateway output with empty env names", () => { + const inputConfig = { + mcpServers: { + sentry: { + type: "stdio", + command: "npx", + env: { + SENTRY_ACCESS_TOKEN: "${SENTRY_ACCESS_TOKEN}", + SENTRY_HOST: "sentry.io", + OPENAI_API_KEY: "${SENTRY_OPENAI_API_KEY}", + }, + }, + grafana: { + type: "stdio", + container: "grafana/mcp-grafana:1.0.0-alpine", + env: { + GRAFANA_URL: "${GRAFANA_URL}", + GRAFANA_SERVICE_ACCOUNT_TOKEN: "${GRAFANA_SERVICE_ACCOUNT_TOKEN}", + }, + }, + github: { + type: "http", + url: "http://localhost:8080/mcp/github", + }, + }, + }; + const gatewayOutput = { + mcpServers: { + github: { + type: "http", + url: "http://localhost:8080/mcp/github", + }, + }, + }; + + expect( + findOmittedRequiredMCPServers(inputConfig, gatewayOutput, [], { + SENTRY_ACCESS_TOKEN: "", + SENTRY_OPENAI_API_KEY: "", + GRAFANA_URL: "", + GRAFANA_SERVICE_ACCOUNT_TOKEN: "", + }) + ).toEqual([ + { name: "sentry", missingEnvVars: ["SENTRY_ACCESS_TOKEN", "SENTRY_OPENAI_API_KEY"] }, + { name: "grafana", missingEnvVars: ["GRAFANA_SERVICE_ACCOUNT_TOKEN", "GRAFANA_URL"] }, + ]); + }); + + it("does not report optional servers omitted from gateway output", () => { + const inputConfig = { + mcpServers: { + datadog: { type: "http", url: "https://example.com/mcp", required: false }, + slack: { type: "http", url: "https://example.com/slack" }, + }, + }; + const gatewayOutput = { mcpServers: {} }; + + expect(findOmittedRequiredMCPServers(inputConfig, gatewayOutput, ["slack"])).toEqual([]); + }); + + it("formats an actionable startup failure message", () => { + expect( + formatOmittedRequiredMCPServersMessage([ + { name: "sentry", missingEnvVars: ["SENTRY_ACCESS_TOKEN"] }, + { name: "grafana", missingEnvVars: ["GRAFANA_SERVICE_ACCOUNT_TOKEN", "GRAFANA_URL"] }, + ]) + ).toBe( + "Required MCP server(s) were omitted from the gateway output: sentry (missing/empty env: SENTRY_ACCESS_TOKEN); grafana (missing/empty env: GRAFANA_SERVICE_ACCOUNT_TOKEN, GRAFANA_URL). Configure the missing secrets/environment variables, or mark intentionally best-effort servers with required: false." + ); + }); + + it("detects direct empty env values when no variable placeholder is present", () => { + expect( + getMissingEnvVarNamesForServer({ + env: { + API_TOKEN: "", + HOST: "https://example.com", + }, + }) + ).toEqual(["API_TOKEN"]); + }); +}); From 1a5391399aa2362ddde3abde01d19e465918ee0a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 15 Aug 2026 06:00:07 +0000 Subject: [PATCH 3/4] Address MCP omission review feedback Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/start_mcp_gateway.cjs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/actions/setup/js/start_mcp_gateway.cjs b/actions/setup/js/start_mcp_gateway.cjs index 5b894e34533..4917464848e 100644 --- a/actions/setup/js/start_mcp_gateway.cjs +++ b/actions/setup/js/start_mcp_gateway.cjs @@ -299,7 +299,8 @@ function getMissingEnvVarNamesForServer(serverConfig, env = process.env) { } for (const match of matches) { const varName = match[1]; - if (!env[varName] || env[varName].trim() === "") { + const envValue = env[varName]; + if (envValue == null || envValue.trim() === "") { missing.add(varName); } } @@ -974,7 +975,7 @@ async function main() { const omittedRequiredServers = findOmittedRequiredMCPServers(configObj, gatewayOutput, optionalServerNames); if (omittedRequiredServers.length > 0) { const message = formatOmittedRequiredMCPServersMessage(omittedRequiredServers); - core.error(`ERROR: ${message}`); + core.error(message); try { process.kill(gatewayPid); } catch { From 45f20740786f4ef5ad35e86a396e678e13f15b27 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 15 Aug 2026 06:01:34 +0000 Subject: [PATCH 4/4] Refine MCP omission validation tests Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/start_mcp_gateway.cjs | 1 - actions/setup/js/start_mcp_gateway.test.cjs | 12 +++++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/actions/setup/js/start_mcp_gateway.cjs b/actions/setup/js/start_mcp_gateway.cjs index 4917464848e..697c4d1c247 100644 --- a/actions/setup/js/start_mcp_gateway.cjs +++ b/actions/setup/js/start_mcp_gateway.cjs @@ -975,7 +975,6 @@ async function main() { const omittedRequiredServers = findOmittedRequiredMCPServers(configObj, gatewayOutput, optionalServerNames); if (omittedRequiredServers.length > 0) { const message = formatOmittedRequiredMCPServersMessage(omittedRequiredServers); - core.error(message); try { process.kill(gatewayPid); } catch { diff --git a/actions/setup/js/start_mcp_gateway.test.cjs b/actions/setup/js/start_mcp_gateway.test.cjs index ed773ac8b23..dcec2e0432f 100644 --- a/actions/setup/js/start_mcp_gateway.test.cjs +++ b/actions/setup/js/start_mcp_gateway.test.cjs @@ -405,10 +405,20 @@ describe("start_mcp_gateway omitted required MCP server detection", () => { ]); }); - it("does not report optional servers omitted from gateway output", () => { + it("does not report servers marked optional in their config", () => { const inputConfig = { mcpServers: { datadog: { type: "http", url: "https://example.com/mcp", required: false }, + }, + }; + const gatewayOutput = { mcpServers: {} }; + + expect(findOmittedRequiredMCPServers(inputConfig, gatewayOutput)).toEqual([]); + }); + + it("does not report servers forwarded as optional after required flags are stripped", () => { + const inputConfig = { + mcpServers: { slack: { type: "http", url: "https://example.com/slack" }, }, };