Mcp

MCP Server Configuration Management

Complete guide to configuration management for MCP servers, covering environment variables, configuration files, runtime configuration updates, feature flags, multi-environment support, secrets management, configuration validation, and building configurable MCP server infrastructure.

MCP Server Configuration Management

Overview

Configuration is how you tell your MCP server what to do without changing its code. Which database to connect to, how many connections to pool, which tools to enable, what rate limits to enforce — all of this lives in configuration. I have seen MCP servers deployed with hardcoded database credentials, servers that required a code change to adjust a timeout, and servers where nobody could figure out which configuration values were actually in effect. A solid configuration strategy prevents all of this.

Prerequisites

  • Node.js 16 or later
  • @modelcontextprotocol/sdk package installed
  • dotenv package for local development (npm install dotenv)
  • Basic understanding of environment variables
  • Familiarity with MCP server architecture
  • Understanding of deployment environments (development, staging, production)

Environment Variable Configuration

Basic Environment Loading

Environment variables are the simplest and most portable configuration mechanism. They work everywhere — Docker, Kubernetes, PM2, systemd, and local development.

// config/env.js
var path = require("path");

// Load .env file in development only
if (process.env.NODE_ENV !== "production") {
  try {
    require("dotenv").config({ path: path.resolve(process.cwd(), ".env") });
  } catch (e) {
    // dotenv not installed in production — that is fine
  }
}

function requireEnv(name) {
  var value = process.env[name];
  if (value === undefined || value === "") {
    throw new Error("Required environment variable missing: " + name);
  }
  return value;
}

function optionalEnv(name, defaultValue) {
  var value = process.env[name];
  if (value === undefined || value === "") {
    return defaultValue;
  }
  return value;
}

function envInt(name, defaultValue) {
  var value = process.env[name];
  if (value === undefined || value === "") return defaultValue;
  var parsed = parseInt(value, 10);
  if (isNaN(parsed)) {
    throw new Error("Environment variable " + name + " must be an integer, got: " + value);
  }
  return parsed;
}

function envBool(name, defaultValue) {
  var value = process.env[name];
  if (value === undefined || value === "") return defaultValue;
  return value === "true" || value === "1" || value === "yes";
}

function envList(name, defaultValue) {
  var value = process.env[name];
  if (value === undefined || value === "") return defaultValue || [];
  return value.split(",").map(function(s) { return s.trim(); }).filter(Boolean);
}

module.exports = {
  requireEnv: requireEnv,
  optionalEnv: optionalEnv,
  envInt: envInt,
  envBool: envBool,
  envList: envList
};

Structured Configuration Object

// config/index.js
var env = require("./env");

var config = {
  server: {
    name: env.optionalEnv("MCP_SERVER_NAME", "mcp-server"),
    version: env.optionalEnv("MCP_SERVER_VERSION", "1.0.0"),
    port: env.envInt("MCP_PORT", 3100),
    host: env.optionalEnv("MCP_HOST", "0.0.0.0"),
    env: env.optionalEnv("NODE_ENV", "development")
  },

  database: {
    url: env.optionalEnv("DATABASE_URL", null),
    poolMin: env.envInt("DB_POOL_MIN", 2),
    poolMax: env.envInt("DB_POOL_MAX", 10),
    idleTimeout: env.envInt("DB_IDLE_TIMEOUT", 30000),
    connectionTimeout: env.envInt("DB_CONNECTION_TIMEOUT", 5000),
    ssl: env.envBool("DB_SSL", false)
  },

  auth: {
    enabled: env.envBool("AUTH_ENABLED", true),
    jwtSecret: env.optionalEnv("JWT_SECRET", null),
    apiKeys: env.envList("API_KEYS"),
    allowAnonymous: env.envBool("ALLOW_ANONYMOUS", false)
  },

  limits: {
    maxConnections: env.envInt("MAX_CONNECTIONS", 50),
    rateLimit: env.envInt("RATE_LIMIT", 100),
    rateLimitWindow: env.envInt("RATE_LIMIT_WINDOW", 60000),
    requestTimeout: env.envInt("REQUEST_TIMEOUT", 30000),
    maxResponseSize: env.envInt("MAX_RESPONSE_SIZE", 1048576)  // 1MB
  },

  logging: {
    level: env.optionalEnv("LOG_LEVEL", "info"),
    format: env.optionalEnv("LOG_FORMAT", "text"),
    file: env.optionalEnv("LOG_FILE", null)
  },

  tools: {
    enabled: env.envList("ENABLED_TOOLS"),
    disabled: env.envList("DISABLED_TOOLS"),
    queryTimeout: env.envInt("TOOL_QUERY_TIMEOUT", 10000),
    fileRoot: env.optionalEnv("FILE_ROOT", process.cwd())
  },

  cors: {
    origins: env.envList("CORS_ORIGINS", ["http://localhost:3000"]),
    credentials: env.envBool("CORS_CREDENTIALS", true)
  },

  metrics: {
    enabled: env.envBool("METRICS_ENABLED", false),
    port: env.envInt("METRICS_PORT", 9090)
  }
};

// Freeze to prevent accidental mutation
function deepFreeze(obj) {
  Object.freeze(obj);
  Object.keys(obj).forEach(function(key) {
    if (typeof obj[key] === "object" && obj[key] !== null && !Array.isArray(obj[key])) {
      deepFreeze(obj[key]);
    }
  });
  return obj;
}

module.exports = deepFreeze(config);

Environment File Templates

# .env.example — copy to .env for local development
NODE_ENV=development
MCP_PORT=3100
MCP_SERVER_NAME=my-mcp-server

# Database
DATABASE_URL=postgresql://localhost:5432/mcpdev
DB_POOL_MIN=1
DB_POOL_MAX=5

# Authentication
AUTH_ENABLED=false
JWT_SECRET=dev-secret-do-not-use-in-production
ALLOW_ANONYMOUS=true

# Tool Configuration
ENABLED_TOOLS=echo,query,list_files,read_file
FILE_ROOT=./workspace

# Logging
LOG_LEVEL=debug
LOG_FORMAT=text

# CORS
CORS_ORIGINS=http://localhost:3000,http://localhost:5173
# .env.production — reference for production deployment
NODE_ENV=production
MCP_PORT=3100

# Database (set via deployment secrets, not in file)
# DATABASE_URL=postgresql://...
DB_POOL_MIN=5
DB_POOL_MAX=20
DB_SSL=true

# Authentication (set via deployment secrets)
AUTH_ENABLED=true
# JWT_SECRET=...
ALLOW_ANONYMOUS=false

# Tool Configuration
DISABLED_TOOLS=debug_info,shell_exec
FILE_ROOT=/opt/mcp/workspace

# Logging
LOG_LEVEL=info
LOG_FORMAT=json
LOG_FILE=/var/log/mcp/server.log

# Limits
MAX_CONNECTIONS=200
RATE_LIMIT=50
REQUEST_TIMEOUT=15000

# Metrics
METRICS_ENABLED=true

Configuration File Support

JSON Configuration Files

For complex configurations that do not fit well in environment variables — like tool-specific settings, permission matrices, or resource definitions — use configuration files.

// config/file-loader.js
var fs = require("fs");
var path = require("path");

function loadConfigFile(filePath) {
  var resolved = path.resolve(filePath);

  if (!fs.existsSync(resolved)) {
    return null;
  }

  var ext = path.extname(resolved).toLowerCase();
  var content = fs.readFileSync(resolved, "utf8");

  if (ext === ".json") {
    try {
      return JSON.parse(content);
    } catch (err) {
      throw new Error("Invalid JSON in " + resolved + ": " + err.message);
    }
  }

  throw new Error("Unsupported config file format: " + ext);
}

function loadConfigWithOverrides(defaults, filePath, envOverrides) {
  var fileConfig = loadConfigFile(filePath) || {};

  // Deep merge: defaults < file config < env overrides
  return deepMerge(deepMerge(defaults, fileConfig), envOverrides);
}

function deepMerge(target, source) {
  var result = {};

  Object.keys(target).forEach(function(key) {
    result[key] = target[key];
  });

  Object.keys(source).forEach(function(key) {
    if (source[key] === null || source[key] === undefined) return;

    if (typeof source[key] === "object" && !Array.isArray(source[key]) &&
        typeof result[key] === "object" && !Array.isArray(result[key])) {
      result[key] = deepMerge(result[key] || {}, source[key]);
    } else {
      result[key] = source[key];
    }
  });

  return result;
}

module.exports = {
  loadConfigFile: loadConfigFile,
  loadConfigWithOverrides: loadConfigWithOverrides,
  deepMerge: deepMerge
};

Tool Configuration File

{
  "tools": {
    "query": {
      "enabled": true,
      "allowedSchemas": ["public", "analytics"],
      "blockedTables": ["users_private", "api_keys"],
      "maxRows": 500,
      "timeout": 10000
    },
    "read_file": {
      "enabled": true,
      "allowedExtensions": [".js", ".ts", ".json", ".md", ".txt", ".yaml", ".yml"],
      "maxFileSize": 1048576,
      "blockedPaths": ["node_modules", ".git", ".env"]
    },
    "http_get": {
      "enabled": true,
      "allowedDomains": ["api.example.com", "data.example.com"],
      "timeout": 5000,
      "maxResponseSize": 524288
    },
    "shell_exec": {
      "enabled": false,
      "allowedCommands": ["ls", "cat", "grep", "wc"],
      "timeout": 5000
    }
  },
  "permissions": {
    "admin": {
      "tools": "*",
      "maxRows": 10000
    },
    "developer": {
      "tools": ["query", "read_file", "list_files", "http_get"],
      "maxRows": 1000
    },
    "viewer": {
      "tools": ["query", "list_files"],
      "maxRows": 100
    }
  }
}

Loading Tool Configuration

// config/tool-config.js
var fileLoader = require("./file-loader");
var path = require("path");

function loadToolConfig() {
  var configPath = process.env.TOOL_CONFIG_PATH ||
                   path.join(process.cwd(), "config", "tools.json");

  var config = fileLoader.loadConfigFile(configPath);

  if (!config) {
    console.log("No tool configuration file found at " + configPath + ". Using defaults.");
    return getDefaults();
  }

  console.log("Loaded tool configuration from " + configPath);
  return config;
}

function getDefaults() {
  return {
    tools: {
      query: { enabled: true, maxRows: 500, timeout: 10000 },
      read_file: { enabled: true, maxFileSize: 1048576 },
      list_files: { enabled: true },
      echo: { enabled: true }
    },
    permissions: {
      admin: { tools: "*" },
      user: { tools: ["query", "read_file", "list_files", "echo"] }
    }
  };
}

function isToolEnabled(toolConfig, toolName) {
  if (!toolConfig.tools[toolName]) return true;  // Unknown tools enabled by default
  return toolConfig.tools[toolName].enabled !== false;
}

function getToolSetting(toolConfig, toolName, setting, defaultValue) {
  if (!toolConfig.tools[toolName]) return defaultValue;
  var value = toolConfig.tools[toolName][setting];
  return value !== undefined ? value : defaultValue;
}

function userCanUseTool(toolConfig, role, toolName) {
  var perms = toolConfig.permissions[role];
  if (!perms) return false;
  if (perms.tools === "*") return true;
  return perms.tools.indexOf(toolName) > -1;
}

module.exports = {
  loadToolConfig: loadToolConfig,
  isToolEnabled: isToolEnabled,
  getToolSetting: getToolSetting,
  userCanUseTool: userCanUseTool
};

Configuration Validation

Startup Validation

Fail fast. If configuration is invalid, crash on startup with a clear error message — not 30 minutes later when the first request triggers a code path that reads the bad value.

// config/validator.js
function validateConfig(config) {
  var errors = [];
  var warnings = [];

  // Server validation
  if (config.server.port < 1 || config.server.port > 65535) {
    errors.push("server.port must be between 1 and 65535, got: " + config.server.port);
  }

  // Database validation
  if (config.database.url) {
    if (!config.database.url.startsWith("postgresql://") && !config.database.url.startsWith("postgres://")) {
      errors.push("database.url must start with postgresql:// or postgres://");
    }
    if (config.database.poolMin > config.database.poolMax) {
      errors.push("database.poolMin (" + config.database.poolMin + ") cannot exceed poolMax (" + config.database.poolMax + ")");
    }
  }

  // Auth validation
  if (config.server.env === "production") {
    if (!config.auth.enabled) {
      warnings.push("Authentication is disabled in production — this is a security risk");
    }
    if (config.auth.jwtSecret && config.auth.jwtSecret.length < 32) {
      errors.push("JWT_SECRET must be at least 32 characters in production");
    }
    if (config.auth.allowAnonymous) {
      warnings.push("Anonymous access is enabled in production");
    }
  }

  // Limits validation
  if (config.limits.maxConnections < 1) {
    errors.push("limits.maxConnections must be at least 1");
  }
  if (config.limits.rateLimit < 1) {
    errors.push("limits.rateLimit must be at least 1");
  }
  if (config.limits.requestTimeout < 1000) {
    warnings.push("limits.requestTimeout is very low (" + config.limits.requestTimeout + "ms) — tool invocations may timeout");
  }

  // Logging validation
  var validLevels = ["error", "warn", "info", "debug", "verbose"];
  if (validLevels.indexOf(config.logging.level) === -1) {
    errors.push("logging.level must be one of: " + validLevels.join(", ") + ", got: " + config.logging.level);
  }

  // Tools validation
  if (config.tools.fileRoot) {
    var fs = require("fs");
    if (!fs.existsSync(config.tools.fileRoot)) {
      warnings.push("tools.fileRoot does not exist: " + config.tools.fileRoot);
    }
  }

  return { errors: errors, warnings: warnings, valid: errors.length === 0 };
}

function validateAndExit(config) {
  var result = validateConfig(config);

  result.warnings.forEach(function(warning) {
    console.warn("CONFIG WARNING: " + warning);
  });

  if (!result.valid) {
    console.error("\nConfiguration validation failed:");
    result.errors.forEach(function(error) {
      console.error("  ERROR: " + error);
    });
    console.error("\nExiting due to invalid configuration.");
    process.exit(1);
  }

  console.log("Configuration validated successfully" +
              (result.warnings.length > 0 ? " (" + result.warnings.length + " warnings)" : ""));
}

module.exports = {
  validateConfig: validateConfig,
  validateAndExit: validateAndExit
};

Runtime Configuration Updates

Hot Reloading Configuration

Some configuration changes should take effect without restarting the server — log levels, rate limits, feature flags, tool enable/disable.

// config/hot-reload.js
var fs = require("fs");
var path = require("path");
var EventEmitter = require("events");

function ConfigWatcher(configPath) {
  this.configPath = path.resolve(configPath);
  this.currentConfig = null;
  this.events = new EventEmitter();
  this.watcher = null;
}

ConfigWatcher.prototype.start = function() {
  var self = this;

  // Load initial config
  this.currentConfig = this._loadFile();

  // Watch for changes
  this.watcher = fs.watch(this.configPath, function(eventType) {
    if (eventType === "change") {
      // Debounce — file editors often trigger multiple events
      clearTimeout(self._debounceTimer);
      self._debounceTimer = setTimeout(function() {
        self._reload();
      }, 500);
    }
  });

  console.log("Watching config file: " + this.configPath);
  return this.currentConfig;
};

ConfigWatcher.prototype._loadFile = function() {
  try {
    var content = fs.readFileSync(this.configPath, "utf8");
    return JSON.parse(content);
  } catch (err) {
    console.error("Failed to load config file: " + err.message);
    return null;
  }
};

ConfigWatcher.prototype._reload = function() {
  var newConfig = this._loadFile();
  if (!newConfig) return;

  var changes = this._diff(this.currentConfig, newConfig);
  if (changes.length === 0) return;

  var oldConfig = this.currentConfig;
  this.currentConfig = newConfig;

  console.log("Configuration reloaded. Changes: " + changes.join(", "));
  this.events.emit("change", {
    oldConfig: oldConfig,
    newConfig: newConfig,
    changes: changes
  });
};

ConfigWatcher.prototype._diff = function(oldObj, newObj, prefix) {
  var changes = [];
  prefix = prefix || "";

  var allKeys = {};
  Object.keys(oldObj || {}).forEach(function(k) { allKeys[k] = true; });
  Object.keys(newObj || {}).forEach(function(k) { allKeys[k] = true; });

  Object.keys(allKeys).forEach(function(key) {
    var path = prefix ? prefix + "." + key : key;
    var oldVal = oldObj ? oldObj[key] : undefined;
    var newVal = newObj ? newObj[key] : undefined;

    if (typeof oldVal === "object" && typeof newVal === "object" &&
        oldVal !== null && newVal !== null && !Array.isArray(oldVal)) {
      changes = changes.concat(this._diff(oldVal, newVal, path));
    } else if (JSON.stringify(oldVal) !== JSON.stringify(newVal)) {
      changes.push(path);
    }
  }.bind(this));

  return changes;
};

ConfigWatcher.prototype.stop = function() {
  if (this.watcher) {
    this.watcher.close();
    this.watcher = null;
  }
};

ConfigWatcher.prototype.get = function(key) {
  var parts = key.split(".");
  var value = this.currentConfig;

  for (var i = 0; i < parts.length; i++) {
    if (value === null || value === undefined) return undefined;
    value = value[parts[i]];
  }

  return value;
};

module.exports = ConfigWatcher;

Applying Runtime Changes

// config/runtime-updater.js
var logger = require("../logger");

function createRuntimeUpdater(configWatcher, components) {
  configWatcher.events.on("change", function(event) {
    var changes = event.changes;
    var newConfig = event.newConfig;

    changes.forEach(function(changePath) {
      logger.info("Config changed: " + changePath);

      // Update log level dynamically
      if (changePath === "logging.level") {
        var newLevel = newConfig.logging.level;
        if (components.logger) {
          components.logger.level = newLevel;
          logger.info("Log level changed to: " + newLevel);
        }
      }

      // Update rate limits
      if (changePath.startsWith("limits.rateLimit")) {
        if (components.rateLimiter) {
          components.rateLimiter.setLimit(newConfig.limits.rateLimit);
          logger.info("Rate limit changed to: " + newConfig.limits.rateLimit);
        }
      }

      // Enable/disable tools
      if (changePath.startsWith("tools.")) {
        var toolMatch = changePath.match(/^tools\.(\w+)\.enabled$/);
        if (toolMatch && components.toolManager) {
          var toolName = toolMatch[1];
          var enabled = newConfig.tools[toolName].enabled;
          components.toolManager.setEnabled(toolName, enabled);
          logger.info("Tool " + toolName + " " + (enabled ? "enabled" : "disabled"));
        }
      }

      // Update max connections
      if (changePath === "limits.maxConnections") {
        if (components.connectionManager) {
          components.connectionManager.setMaxConnections(newConfig.limits.maxConnections);
          logger.info("Max connections changed to: " + newConfig.limits.maxConnections);
        }
      }
    });
  });
}

module.exports = createRuntimeUpdater;

Feature Flags

Simple Feature Flag System

Feature flags let you deploy code and enable features independently. Ship the code for a new tool on Monday, enable it for testing on Tuesday, roll it out to all users on Wednesday.

// config/feature-flags.js
function FeatureFlags(initialFlags) {
  this.flags = {};
  var self = this;

  Object.keys(initialFlags || {}).forEach(function(key) {
    self.flags[key] = {
      enabled: initialFlags[key].enabled || false,
      description: initialFlags[key].description || "",
      rolloutPercent: initialFlags[key].rolloutPercent || 100,
      allowedRoles: initialFlags[key].allowedRoles || null,
      enabledAt: initialFlags[key].enabled ? new Date() : null
    };
  });
}

FeatureFlags.prototype.isEnabled = function(flagName, context) {
  var flag = this.flags[flagName];
  if (!flag) return false;
  if (!flag.enabled) return false;

  // Role-based gating
  if (flag.allowedRoles && context && context.role) {
    if (flag.allowedRoles.indexOf(context.role) === -1) {
      return false;
    }
  }

  // Percentage rollout
  if (flag.rolloutPercent < 100 && context && context.userId) {
    var hash = simpleHash(context.userId + flagName);
    if ((hash % 100) >= flag.rolloutPercent) {
      return false;
    }
  }

  return true;
};

FeatureFlags.prototype.setFlag = function(flagName, enabled) {
  if (!this.flags[flagName]) {
    this.flags[flagName] = {
      enabled: enabled,
      description: "",
      rolloutPercent: 100,
      allowedRoles: null,
      enabledAt: enabled ? new Date() : null
    };
  } else {
    this.flags[flagName].enabled = enabled;
    this.flags[flagName].enabledAt = enabled ? new Date() : null;
  }
};

FeatureFlags.prototype.getAll = function() {
  var result = {};
  var self = this;
  Object.keys(this.flags).forEach(function(key) {
    result[key] = {
      enabled: self.flags[key].enabled,
      description: self.flags[key].description,
      rolloutPercent: self.flags[key].rolloutPercent,
      enabledAt: self.flags[key].enabledAt
    };
  });
  return result;
};

function simpleHash(str) {
  var hash = 0;
  for (var i = 0; i < str.length; i++) {
    var char = str.charCodeAt(i);
    hash = ((hash << 5) - hash) + char;
    hash = hash & hash;
  }
  return Math.abs(hash);
}

module.exports = FeatureFlags;

Feature-Flagged Tool Registration

// tools/flagged-tools.js
function registerFlaggedTools(mcpServer, featureFlags, dependencies) {
  // Always-on tool
  mcpServer.tool("echo", "Echo a message", {
    message: { type: "string" }
  }, function(params) {
    return { content: [{ type: "text", text: "Echo: " + params.message }] };
  });

  // Feature-flagged tool
  mcpServer.tool("advanced_query", "Run an advanced analytical query", {
    sql: { type: "string" }
  }, function(params, context) {
    if (!featureFlags.isEnabled("advanced_queries", {
      role: context.userRole,
      userId: context.userId
    })) {
      return {
        content: [{ type: "text", text: "This feature is not yet available for your account." }],
        isError: true
      };
    }

    return dependencies.db.query(params.sql).then(function(result) {
      return { content: [{ type: "text", text: JSON.stringify(result.rows, null, 2) }] };
    });
  });

  // Beta tool behind flag
  mcpServer.tool("ai_suggest", "Get AI-powered suggestions", {
    prompt: { type: "string" }
  }, function(params, context) {
    if (!featureFlags.isEnabled("ai_suggestions", {
      role: context.userRole,
      userId: context.userId
    })) {
      return {
        content: [{ type: "text", text: "AI suggestions are in beta. Contact admin for access." }],
        isError: true
      };
    }

    // Beta implementation
    return {
      content: [{ type: "text", text: "Suggestion for: " + params.prompt + "\n(Beta feature)" }]
    };
  });
}

module.exports = registerFlaggedTools;

Multi-Environment Support

Environment-Specific Configuration

// config/environments.js
var environments = {
  development: {
    server: { port: 3100 },
    database: { poolMin: 1, poolMax: 5, ssl: false },
    auth: { enabled: false, allowAnonymous: true },
    logging: { level: "debug", format: "text" },
    limits: { maxConnections: 10, rateLimit: 1000 },
    features: {
      debug_tools: { enabled: true },
      advanced_queries: { enabled: true },
      ai_suggestions: { enabled: true, rolloutPercent: 100 }
    }
  },

  staging: {
    server: { port: 3100 },
    database: { poolMin: 2, poolMax: 10, ssl: true },
    auth: { enabled: true, allowAnonymous: false },
    logging: { level: "debug", format: "json" },
    limits: { maxConnections: 50, rateLimit: 200 },
    features: {
      debug_tools: { enabled: true },
      advanced_queries: { enabled: true },
      ai_suggestions: { enabled: true, rolloutPercent: 50 }
    }
  },

  production: {
    server: { port: 3100 },
    database: { poolMin: 5, poolMax: 20, ssl: true },
    auth: { enabled: true, allowAnonymous: false },
    logging: { level: "info", format: "json" },
    limits: { maxConnections: 200, rateLimit: 100 },
    features: {
      debug_tools: { enabled: false },
      advanced_queries: { enabled: true },
      ai_suggestions: { enabled: false }
    }
  }
};

function getEnvironmentConfig(envName) {
  var env = envName || process.env.NODE_ENV || "development";
  var config = environments[env];

  if (!config) {
    console.warn("Unknown environment: " + env + ". Falling back to development.");
    config = environments.development;
  }

  return config;
}

module.exports = {
  environments: environments,
  getEnvironmentConfig: getEnvironmentConfig
};

Secrets Management

Separating Secrets from Configuration

// config/secrets.js
var fs = require("fs");
var path = require("path");

function loadSecrets() {
  var secrets = {};

  // Strategy 1: Environment variables (highest priority)
  if (process.env.JWT_SECRET) {
    secrets.jwtSecret = process.env.JWT_SECRET;
  }
  if (process.env.DATABASE_URL) {
    secrets.databaseUrl = process.env.DATABASE_URL;
  }
  if (process.env.API_KEYS) {
    secrets.apiKeys = process.env.API_KEYS.split(",").map(function(k) { return k.trim(); });
  }

  // Strategy 2: Docker secrets (files mounted at /run/secrets/)
  var secretsDir = process.env.SECRETS_DIR || "/run/secrets";
  if (fs.existsSync(secretsDir)) {
    var secretFiles = {
      "jwt_secret": "jwtSecret",
      "database_url": "databaseUrl",
      "api_keys": "apiKeys"
    };

    Object.keys(secretFiles).forEach(function(fileName) {
      var filePath = path.join(secretsDir, fileName);
      if (fs.existsSync(filePath)) {
        var value = fs.readFileSync(filePath, "utf8").trim();
        var key = secretFiles[fileName];

        if (key === "apiKeys") {
          secrets[key] = value.split("\n").map(function(k) { return k.trim(); }).filter(Boolean);
        } else {
          secrets[key] = value;
        }
      }
    });
  }

  // Strategy 3: Encrypted secrets file
  var encryptedPath = process.env.ENCRYPTED_SECRETS_PATH;
  if (encryptedPath && fs.existsSync(encryptedPath)) {
    var encryptionKey = process.env.SECRETS_ENCRYPTION_KEY;
    if (encryptionKey) {
      var decrypted = decryptSecrets(encryptedPath, encryptionKey);
      Object.keys(decrypted).forEach(function(key) {
        if (!secrets[key]) {
          secrets[key] = decrypted[key];
        }
      });
    }
  }

  return secrets;
}

function decryptSecrets(filePath, key) {
  var crypto = require("crypto");
  var encrypted = fs.readFileSync(filePath, "utf8");
  var parts = encrypted.split(":");
  var iv = Buffer.from(parts[0], "hex");
  var data = Buffer.from(parts[1], "hex");

  var keyHash = crypto.createHash("sha256").update(key).digest();
  var decipher = crypto.createDecipheriv("aes-256-cbc", keyHash, iv);
  var decrypted = decipher.update(data, undefined, "utf8") + decipher.final("utf8");

  return JSON.parse(decrypted);
}

// Utility to encrypt secrets file
function encryptSecrets(secrets, key, outputPath) {
  var crypto = require("crypto");
  var iv = crypto.randomBytes(16);
  var keyHash = crypto.createHash("sha256").update(key).digest();
  var cipher = crypto.createCipheriv("aes-256-cbc", keyHash, iv);

  var json = JSON.stringify(secrets);
  var encrypted = cipher.update(json, "utf8", "hex") + cipher.final("hex");
  var output = iv.toString("hex") + ":" + encrypted;

  fs.writeFileSync(outputPath, output, "utf8");
  console.log("Secrets encrypted to: " + outputPath);
}

module.exports = {
  loadSecrets: loadSecrets,
  encryptSecrets: encryptSecrets
};

Complete Working Example

A fully configurable MCP server with environment variables, config file loading, validation, feature flags, and runtime configuration updates.

// server.js - Configurable MCP Server
var express = require("express");
var McpServer = require("@modelcontextprotocol/sdk/server/mcp.js").McpServer;
var SSEServerTransport = require("@modelcontextprotocol/sdk/server/sse.js").SSEServerTransport;

// Configuration loading
var config = require("./config");
var validator = require("./config/validator");
var FeatureFlags = require("./config/feature-flags");
var ConfigWatcher = require("./config/hot-reload");
var secrets = require("./config/secrets").loadSecrets();

// Validate configuration on startup
validator.validateAndExit(config);

// Initialize feature flags
var featureFlags = new FeatureFlags({
  debug_tools: {
    enabled: config.server.env !== "production",
    description: "Developer debugging tools"
  },
  advanced_queries: {
    enabled: true,
    description: "Advanced SQL query capabilities",
    allowedRoles: ["admin", "developer"]
  },
  beta_tools: {
    enabled: false,
    description: "Beta features in testing",
    rolloutPercent: 10
  }
});

// Watch config file for runtime changes
var configFile = process.env.CONFIG_FILE || "./config/runtime.json";
var watcher = new ConfigWatcher(configFile);
var runtimeConfig = watcher.start();

watcher.events.on("change", function(event) {
  console.log("Runtime config changed:", event.changes.join(", "));

  // Apply feature flag changes
  if (event.newConfig && event.newConfig.features) {
    Object.keys(event.newConfig.features).forEach(function(flag) {
      featureFlags.setFlag(flag, event.newConfig.features[flag].enabled);
    });
  }
});

// Express setup
var app = express();
app.use(express.json());

// MCP server
var mcpServer = new McpServer({
  name: config.server.name,
  version: config.server.version
});

// Register tools based on configuration
mcpServer.tool("echo", "Echo a message", {
  message: { type: "string" }
}, function(params) {
  return { content: [{ type: "text", text: "Echo: " + params.message }] };
});

mcpServer.tool("get_config", "Show current server configuration (non-sensitive)", {}, function() {
  return {
    content: [{
      type: "text",
      text: JSON.stringify({
        server: config.server,
        limits: config.limits,
        logging: { level: config.logging.level, format: config.logging.format },
        tools: { enabled: config.tools.enabled, disabled: config.tools.disabled },
        features: featureFlags.getAll()
      }, null, 2)
    }]
  };
});

mcpServer.tool("debug_info", "Get debug information", {}, function(params, context) {
  if (!featureFlags.isEnabled("debug_tools", context)) {
    return {
      content: [{ type: "text", text: "Debug tools are disabled in this environment" }],
      isError: true
    };
  }

  return {
    content: [{
      type: "text",
      text: JSON.stringify({
        env: config.server.env,
        uptime: Math.floor(process.uptime()) + "s",
        memory: process.memoryUsage(),
        pid: process.pid,
        nodeVersion: process.version,
        runtimeConfig: runtimeConfig
      }, null, 2)
    }]
  };
});

// SSE transport
var sessions = {};

app.get("/health", function(req, res) {
  res.json({
    status: "healthy",
    config: {
      env: config.server.env,
      name: config.server.name,
      version: config.server.version
    }
  });
});

app.get("/config/features", function(req, res) {
  res.json(featureFlags.getAll());
});

app.post("/config/features/:name", express.json(), function(req, res) {
  var name = req.params.name;
  var enabled = req.body.enabled;

  if (typeof enabled !== "boolean") {
    res.status(400).json({ error: "enabled must be a boolean" });
    return;
  }

  featureFlags.setFlag(name, enabled);
  console.log("Feature flag '" + name + "' set to " + enabled);
  res.json({ flag: name, enabled: enabled });
});

app.get("/mcp/sse", function(req, res) {
  if (Object.keys(sessions).length >= config.limits.maxConnections) {
    res.status(429).json({ error: "Connection limit reached" });
    return;
  }

  res.setHeader("Content-Type", "text/event-stream");
  res.setHeader("Cache-Control", "no-cache");
  res.setHeader("Connection", "keep-alive");

  var transport = new SSEServerTransport("/mcp/messages", res);
  var sessionId = transport.sessionId;
  sessions[sessionId] = { transport: transport, connectedAt: new Date() };

  console.log("Session opened: " + sessionId + " (total: " + Object.keys(sessions).length + ")");

  transport.onclose = function() {
    delete sessions[sessionId];
    console.log("Session closed: " + sessionId);
  };

  mcpServer.connect(transport);
});

app.post("/mcp/messages", function(req, res) {
  var session = sessions[req.query.sessionId];
  if (!session) {
    res.status(400).json({ error: "Invalid session" });
    return;
  }
  session.transport.handlePostMessage(req, res);
});

// Start
app.listen(config.server.port, config.server.host, function() {
  console.log("");
  console.log("MCP Server: " + config.server.name + " v" + config.server.version);
  console.log("Environment: " + config.server.env);
  console.log("Listening: http://" + config.server.host + ":" + config.server.port);
  console.log("Auth: " + (config.auth.enabled ? "enabled" : "disabled"));
  console.log("Database: " + (config.database.url ? "configured" : "not configured"));
  console.log("Features: " + JSON.stringify(featureFlags.getAll()));
  console.log("");
});

// Graceful shutdown
process.on("SIGTERM", function() {
  console.log("Shutting down...");
  watcher.stop();
  process.exit(0);
});
# Development
NODE_ENV=development node server.js

# Production with environment variables
NODE_ENV=production \
  MCP_PORT=3100 \
  JWT_SECRET=your-production-secret-here-min-32-chars \
  DATABASE_URL=postgresql://user:pass@db:5432/mcp \
  LOG_LEVEL=info \
  LOG_FORMAT=json \
  METRICS_ENABLED=true \
  node server.js

# Toggle a feature flag at runtime
curl -X POST http://localhost:3100/config/features/beta_tools \
  -H "Content-Type: application/json" \
  -d '{"enabled": true}'

Output:

MCP Server: mcp-server v1.0.0
Environment: production
Listening: http://0.0.0.0:3100
Auth: enabled
Database: configured
Features: {"debug_tools":{"enabled":false},"advanced_queries":{"enabled":true},"beta_tools":{"enabled":false}}

Common Issues and Troubleshooting

Environment Variables Not Loading

Error: Required environment variable missing: DATABASE_URL

The .env file is not being loaded. Check that dotenv is installed and the file path is correct.

// Fix: Verify .env file location and loading
var result = require("dotenv").config({ debug: true });
if (result.error) {
  console.error("dotenv error:", result.error.message);
}
console.log("Loaded variables:", Object.keys(result.parsed || {}));

Configuration Frozen Object Mutation

TypeError: Cannot assign to read only property 'port' of object

The configuration object is frozen with Object.freeze(). This prevents accidental mutations. If you need to change a value at runtime, use a separate mutable runtime config store.

// Wrong
config.server.port = 4000;  // Throws in strict mode

// Right: use a runtime config layer
var runtimeOverrides = { port: 4000 };
var effectivePort = runtimeOverrides.port || config.server.port;

Config File Watcher Triggering Multiple Reloads

Config changed: limits.rateLimit
Config changed: limits.rateLimit
Config changed: limits.rateLimit

Text editors save files in multiple steps (write temp, rename). The debounce timeout may be too short.

// Fix: Increase debounce delay
self._debounceTimer = setTimeout(function() {
  self._reload();
}, 1000);  // 1 second debounce instead of 500ms

Boolean Environment Variables Parsing Incorrectly

Expected: AUTH_ENABLED=false → false
Actual: AUTH_ENABLED=false → true (truthy string)

All environment variables are strings. "false" is truthy in JavaScript. You need explicit parsing.

// Wrong
var authEnabled = process.env.AUTH_ENABLED;  // String "false" is truthy

// Right
var authEnabled = process.env.AUTH_ENABLED === "true";

Best Practices

  • Validate all configuration at startup — fail fast with clear error messages. A server running with invalid configuration will fail unpredictably later.
  • Never commit secrets to source control — use environment variables, Docker secrets, or encrypted secret files. Add .env to .gitignore and provide .env.example with placeholder values.
  • Freeze configuration objects — use Object.freeze() to prevent accidental mutation. Configuration that changes silently mid-request causes bugs that are nearly impossible to trace.
  • Provide sensible defaults for every setting — a developer cloning the repo should be able to run the server with zero configuration for local development. Only production should require explicit configuration.
  • Separate secrets from configuration — configuration describes behavior (port numbers, pool sizes, feature flags). Secrets provide access (passwords, API keys, tokens). They have different lifecycles and different security requirements.
  • Use typed parsing for environment variables — always parse integers with parseInt, booleans with explicit comparison, and lists by splitting on delimiters. Raw process.env values are always strings.
  • Log effective configuration on startup — print the resolved configuration (with secrets redacted) so operators can verify what values are actually in effect.
  • Support runtime configuration updates for operational values — log levels, rate limits, and feature flags should be changeable without restart. Database URLs and port numbers should require a restart.

References

Powered by Contentful