Expand environment variables throughout mcp_settings.json configuration (#384)

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: samanhappy <2755122+samanhappy@users.noreply.github.com>
This commit is contained in:
Copilot
2025-10-26 19:25:53 +08:00
committed by GitHub
parent 5ca5e2ad47
commit f79028ed64
6 changed files with 728 additions and 21 deletions

View File

@@ -99,22 +99,33 @@ export function replaceEnvVars(input: string): string
export function replaceEnvVars(
input: Record<string, any> | string[] | string | undefined,
): Record<string, any> | string[] | string {
// Handle object input
// Handle object input - recursively expand all nested values
if (input && typeof input === 'object' && !Array.isArray(input)) {
const res: Record<string, string> = {}
const res: Record<string, any> = {}
for (const [key, value] of Object.entries(input)) {
if (typeof value === 'string') {
res[key] = expandEnvVars(value)
} else if (typeof value === 'object' && value !== null) {
// Recursively handle nested objects and arrays
res[key] = replaceEnvVars(value as any)
} else {
res[key] = String(value)
// Preserve non-string, non-object values (numbers, booleans, etc.)
res[key] = value
}
}
return res
}
// Handle array input
// Handle array input - recursively expand all elements
if (Array.isArray(input)) {
return input.map((item) => expandEnvVars(item))
return input.map((item) => {
if (typeof item === 'string') {
return expandEnvVars(item)
} else if (typeof item === 'object' && item !== null) {
return replaceEnvVars(item as any)
}
return item
})
}
// Handle string input

View File

@@ -235,6 +235,7 @@ export const createTransportFromConfig = async (name: string, conf: ServerConfig
env['npm_config_registry'] = settings.systemConfig.install.npmRegistry;
}
// Expand environment variables in command
transport = new StdioClientTransport({
cwd: os.homedir(),
command: conf.command,
@@ -379,12 +380,16 @@ export const initializeClientsFromSettings = async (
try {
for (const conf of allServers) {
const { name } = conf;
// Expand environment variables in all configuration values
const expandedConf = replaceEnvVars(conf as any) as ServerConfigWithName;
// Skip disabled servers
if (conf.enabled === false) {
if (expandedConf.enabled === false) {
console.log(`Skipping disabled server: ${name}`);
nextServerInfos.push({
name,
owner: conf.owner,
owner: expandedConf.owner,
status: 'disconnected',
error: null,
tools: [],
@@ -402,7 +407,7 @@ export const initializeClientsFromSettings = async (
if (existingServer && (!serverName || serverName !== name)) {
nextServerInfos.push({
...existingServer,
enabled: conf.enabled === undefined ? true : conf.enabled,
enabled: expandedConf.enabled === undefined ? true : expandedConf.enabled,
});
console.log(`Server '${name}' is already connected.`);
continue;
@@ -410,15 +415,15 @@ export const initializeClientsFromSettings = async (
let transport;
let openApiClient;
if (conf.type === 'openapi') {
if (expandedConf.type === 'openapi') {
// Handle OpenAPI type servers
if (!conf.openapi?.url && !conf.openapi?.schema) {
if (!expandedConf.openapi?.url && !expandedConf.openapi?.schema) {
console.warn(
`Skipping OpenAPI server '${name}': missing OpenAPI specification URL or schema`,
);
nextServerInfos.push({
name,
owner: conf.owner,
owner: expandedConf.owner,
status: 'disconnected',
error: 'Missing OpenAPI specification URL or schema',
tools: [],
@@ -431,20 +436,20 @@ export const initializeClientsFromSettings = async (
// Create server info first and keep reference to it
const serverInfo: ServerInfo = {
name,
owner: conf.owner,
owner: expandedConf.owner,
status: 'connecting',
error: null,
tools: [],
prompts: [],
createTime: Date.now(),
enabled: conf.enabled === undefined ? true : conf.enabled,
config: conf, // Store reference to original config for OpenAPI passthrough headers
enabled: expandedConf.enabled === undefined ? true : expandedConf.enabled,
config: expandedConf, // Store reference to expanded config for OpenAPI passthrough headers
};
nextServerInfos.push(serverInfo);
try {
// Create OpenAPI client instance
openApiClient = new OpenAPIClient(conf);
openApiClient = new OpenAPIClient(expandedConf);
console.log(`Initializing OpenAPI server: ${name}...`);
@@ -480,7 +485,7 @@ export const initializeClientsFromSettings = async (
continue;
}
} else {
transport = await createTransportFromConfig(name, conf);
transport = await createTransportFromConfig(name, expandedConf);
}
const client = new Client(
@@ -504,7 +509,7 @@ export const initializeClientsFromSettings = async (
: undefined;
// Get request options from server configuration, with fallbacks
const serverRequestOptions = conf.options || {};
const serverRequestOptions = expandedConf.options || {};
const requestOptions = {
timeout: serverRequestOptions.timeout || 60000,
resetTimeoutOnProgress: serverRequestOptions.resetTimeoutOnProgress || false,
@@ -514,7 +519,7 @@ export const initializeClientsFromSettings = async (
// Create server info first and keep reference to it
const serverInfo: ServerInfo = {
name,
owner: conf.owner,
owner: expandedConf.owner,
status: 'connecting',
error: null,
tools: [],
@@ -523,10 +528,10 @@ export const initializeClientsFromSettings = async (
transport,
options: requestOptions,
createTime: Date.now(),
config: conf, // Store reference to original config
config: expandedConf, // Store reference to expanded config
};
const pendingAuth = conf.oauth?.pendingAuthorization;
const pendingAuth = expandedConf.oauth?.pendingAuthorization;
if (pendingAuth) {
serverInfo.status = 'oauth_required';
serverInfo.error = null;
@@ -594,7 +599,7 @@ export const initializeClientsFromSettings = async (
serverInfo.error = null;
// Set up keep-alive ping for SSE connections
setupKeepAlive(serverInfo, conf);
setupKeepAlive(serverInfo, expandedConf);
} else {
serverInfo.status = 'disconnected';
serverInfo.error = `Failed to list data: ${dataError} `;