Guide · AWS AppConfig · Feature Flags
Feature Flags for MCP Servers with AWS AppConfig
AppConfig Feature Flag profiles give MCP server operators a structured, validated, rollback-capable primitive for enabling and disabling individual tools in production — without touching your deployment pipeline. Unlike environment variables or freeform JSON, Feature Flag profiles have a built-in schema enforced by AppConfig, percentage-based rollout to limit blast radius, and one-click or alarm-triggered rollback. For the foundational AppConfig setup (Application, Environment, Configuration Profile), see AWS AppConfig for MCP Servers: Dynamic Configuration Without Redeployment. For choosing the right deployment strategy for rolling out flag changes, see AppConfig Deployment Strategies for Safe MCP Server Config Rollouts.
TL;DR
Create a Feature Flag configuration profile (type AWS.AppConfig.FeatureFlags). Define one flag per MCP tool with an enabled boolean attribute. In your handler, call isToolEnabled(toolName) before executing each tool, and return an MCP protocol error (not a thrown exception) when the flag is off. For new tools, use percentageEnabled to roll out to a fraction of invocations. Wire a CloudWatch alarm on tool error rates to AppConfig's rollback configuration so a bad flag deployment reverts automatically within 30–120 seconds.
Why feature flags are a better toggle primitive than env vars for MCP servers
The naive approach to disabling a broken MCP tool is setting an environment variable like TOOL_SEARCH_ENABLED=false and reloading the function. On Lambda, updating an environment variable triggers a function configuration update — existing containers see no change until they are recycled, new cold starts pick up the new value after a 30–90 second propagation window, and in-flight requests on existing containers are not affected. You have no way to force all containers to pick up the change immediately without a full redeployment.
SSM Parameter Store is a step up: you can poll SSM in your handler and pick up changes within your poll interval. But SSM has no validation (a typo becomes the live value), no deployment strategy (the change is atomic and immediate across all pollers), and no built-in rollback (you have to re-put the old value manually).
| Approach | Change propagation | Schema validation | Rollback mechanism | Percentage rollout |
|---|---|---|---|---|
| Environment variable | 30–90s (new containers only) | None | Redeploy previous config | No |
| SSM Parameter Store | Poll interval (15–60s) | None | Manual re-put | No |
| AppConfig Freeform JSON | Poll interval | JSON Schema validator | One-click deployment rollback | No |
| AppConfig Feature Flags | Poll interval | Managed by AppConfig | One-click + alarm-triggered | Yes (percentageEnabled) |
The key advantage of the Feature Flag profile type over a freeform JSON profile is that AppConfig owns the schema. You cannot accidentally save a flag with a string value where a boolean is expected, and the console provides a structured UI for editing flags without touching raw JSON. For MCP tool toggles specifically, this reduces the risk of an operator error during an incident when someone is moving fast.
Creating a Feature Flag profile in AppConfig
In the AppConfig console, create a new Configuration Profile and select type Feature Flag. The profile type identifier is AWS.AppConfig.FeatureFlags. The underlying document is a JSON object with a flags map (one entry per flag) and a values map (the current state of each flag). AppConfig manages the schema; you do not write a JSON Schema validator manually.
The CDK v2 equivalent creates a CfnConfigurationProfile with the correct type and an initial hosted configuration version:
import * as cdk from 'aws-cdk-lib';
import * as appconfig from 'aws-cdk-lib/aws-appconfig';
// Assumes you already have app.ref and prodEnv.ref from the base stack
const featureFlagProfile = new appconfig.CfnConfigurationProfile(this, 'McpFeatureFlags', {
applicationId: app.ref,
name: 'mcp-tool-flags',
locationUri: 'hosted',
type: 'AWS.AppConfig.FeatureFlags',
// Feature flag profiles do not accept JSON Schema validators — AppConfig validates internally
});
// Initial version: all tools enabled by default except executeCode and queryDatabase
const initialFlags = new appconfig.CfnHostedConfigurationVersion(this, 'InitialFlagVersion', {
applicationId: app.ref,
configurationProfileId: featureFlagProfile.ref,
contentType: 'application/json',
content: JSON.stringify({
flags: {
search: { name: 'search' },
summarize: { name: 'summarize' },
fetchUrl: { name: 'fetchUrl' },
executeCode: { name: 'executeCode' },
generateImage: { name: 'generateImage' },
queryDatabase: { name: 'queryDatabase' },
},
values: {
search: { enabled: true },
summarize: { enabled: true },
fetchUrl: { enabled: true },
executeCode: { enabled: false },
generateImage: { enabled: true },
queryDatabase: { enabled: false },
},
version: '1',
}),
});
The flags object declares the available flags and their metadata. The values object holds the current state. When you deploy a new version (by editing the values in the console or via a new CfnHostedConfigurationVersion), AppConfig rolls out the new values according to the deployment strategy attached to the deployment.
MCP tool toggle pattern
The correct pattern for MCP servers is to check the feature flag before executing tool logic and return a structured MCP error — not throw a JavaScript exception — when the tool is disabled. Throwing causes the MCP SDK to emit an unhandled error response; returning a structured error gives the client a clean, parseable signal that the tool is intentionally unavailable.
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { CallToolRequestSchema, ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js';
// Config type for the feature flags document
interface ToolFlags {
flags: Record<string, { name: string }>;
values: Record<string, { enabled: boolean; percentageEnabled?: number }>;
version: string;
}
// Assumes you have a configFetcher that returns the parsed ToolFlags document
// (either from the Lambda Extension or the direct SDK poller)
async function isToolEnabled(
toolName: string,
configFetcher: () => Promise<ToolFlags>,
): Promise<boolean> {
const flags = await configFetcher();
const value = flags.values[toolName];
if (!value) {
// Tool not in flags map — default to disabled (fail-safe)
return false;
}
if (value.percentageEnabled !== undefined) {
// Deterministic per-invocation check: hash the tool name + a stable instance ID
// AppConfig handles this in the managed Feature Flag evaluation, but for
// self-managed freeform profiles you can implement a simple deterministic check:
const hash = simpleHash(toolName + (process.env.AWS_LAMBDA_LOG_STREAM_NAME ?? 'default'));
return hash % 100 < value.percentageEnabled;
}
return value.enabled;
}
function simpleHash(str: string): number {
let h = 0;
for (let i = 0; i < str.length; i++) {
h = (Math.imul(31, h) + str.charCodeAt(i)) | 0;
}
return Math.abs(h);
}
// Register a tool with automatic flag checking
function registerTool(
server: Server,
toolName: string,
handler: (args: unknown) => Promise<unknown>,
configFetcher: () => Promise<ToolFlags>,
) {
server.setRequestHandler(CallToolRequestSchema, async (request) => {
if (request.params.name !== toolName) return undefined; // Let other handlers match
const enabled = await isToolEnabled(toolName, configFetcher);
if (!enabled) {
// Return a structured MCP error — do NOT throw
throw new McpError(
ErrorCode.InvalidRequest,
`Tool '${toolName}' is currently disabled. Please try again later.`,
);
}
const result = await handler(request.params.arguments);
return { content: [{ type: 'text', text: JSON.stringify(result) }] };
});
}
The isToolEnabled helper should be fast — it reads from an in-memory cache populated by the AppConfig poller or Lambda Extension. Never make a synchronous AppConfig API call inside the tool check; that would add 25–40ms per tool invocation.
Percentage rollout for progressive tool enablement
When launching a new MCP tool, you typically want to expose it to a fraction of invocations first, monitor error rates, and expand the rollout if metrics look healthy. AppConfig Feature Flag profiles support a percentageEnabled attribute on each flag value. Set it to 10 to enable the tool for 10% of invocations, then increase through deployments as confidence grows.
AppConfig assigns each execution environment instance to a rollout cohort using a deterministic hash of the environment ID — this means a given Lambda container will consistently evaluate the same flag value across invocations, rather than randomly toggling the tool on and off within a session. This is important for MCP servers because a session may involve multiple sequential tool calls and inconsistent flag values mid-session would confuse clients.
// CDK: deploy a new flag version with 25% rollout for generateImage
const rolloutFlagVersion = new appconfig.CfnHostedConfigurationVersion(this, 'GenerateImageRollout', {
applicationId: app.ref,
configurationProfileId: featureFlagProfile.ref,
contentType: 'application/json',
content: JSON.stringify({
flags: {
search: { name: 'search' },
summarize: { name: 'summarize' },
fetchUrl: { name: 'fetchUrl' },
generateImage: { name: 'generateImage' },
},
values: {
search: { enabled: true },
summarize: { enabled: true },
fetchUrl: { enabled: true },
generateImage: { enabled: true, percentageEnabled: 25 },
},
version: '2',
}),
});
// Deploy with a linear strategy so the rollout itself is gradual
new appconfig.CfnDeployment(this, 'GenerateImageRolloutDeployment', {
applicationId: app.ref,
environmentId: prodEnv.ref,
configurationProfileId: featureFlagProfile.ref,
configurationVersion: rolloutFlagVersion.ref,
deploymentStrategyId: 'AppConfig.Linear10PercentEvery10Minutes',
});
For kill-switch scenarios — disabling a broken tool as fast as possible — use AppConfig.AllAtOnce as the deployment strategy and set enabled: false. Percentage rollout is for cautious enablement of new tools, not for emergency disables where speed matters more than blast-radius control.
Automated rollback via CloudWatch alarms
AppConfig can automatically revert a deployment when a CloudWatch alarm transitions to ALARM state. Wire an alarm on your MCP tool error rate metric to the AppConfig environment's monitors. If the alarm fires during the deployment's bake window, AppConfig transitions to ROLLING_BACK and restores the previous config version without manual intervention.
import * as cloudwatch from 'aws-cdk-lib/aws-cloudwatch';
import * as iam from 'aws-cdk-lib/aws-iam';
// Alarm: MCP tool errors exceed 5% over 5 minutes
const toolErrorAlarm = new cloudwatch.Alarm(this, 'McpToolErrorAlarm', {
metric: new cloudwatch.Metric({
namespace: 'McpServer',
metricName: 'ToolErrors',
dimensionsMap: { Service: 'mcp-server' },
statistic: 'Sum',
period: cdk.Duration.minutes(5),
}),
threshold: 10,
evaluationPeriods: 1,
comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD,
alarmName: 'mcp-server-tool-error-rate',
});
// IAM role for AppConfig to call CloudWatch
const appConfigMonitorRole = new iam.Role(this, 'AppConfigMonitorRole', {
assumedBy: new iam.ServicePrincipal('appconfig.amazonaws.com'),
inlinePolicies: {
CloudWatchRead: new iam.PolicyDocument({
statements: [new iam.PolicyStatement({
actions: ['cloudwatch:DescribeAlarms'],
resources: [toolErrorAlarm.alarmArn],
})],
}),
},
});
// Attach the alarm as a rollback monitor on the AppConfig environment
const prodEnvWithMonitor = new appconfig.CfnEnvironment(this, 'ProdEnvWithMonitor', {
applicationId: app.ref,
name: 'production',
monitors: [
{
alarmArn: toolErrorAlarm.alarmArn,
alarmRoleArn: appConfigMonitorRole.roleArn,
},
],
});
Important: when AppConfig rolls back automatically, it reverts the config to the previous version — but it does NOT mark the new version as permanently invalid. After the rollback, the broken flag version is still available in AppConfig's version history. You must fix the root cause and deploy a new corrected version rather than re-deploying the rolled-back version. The rollback is a recovery, not a fix.
AliveMCP uptime monitoring + AppConfig: complementary layers
AliveMCP's external probe catches the window between a bad flag deployment and a rollback that neither AppConfig nor your internal metrics may surface in time. When AppConfig rolls out a bad flag — say, disabling a tool that was accidentally listed as enabled: false when it should have been true — the deployment strategy determines how fast the bad config reaches all instances. A linear 10%/10min strategy means the full population has the bad config after 100 minutes. AliveMCP probes every 60 seconds using a real JSON-RPC tools/list handshake, so it will detect within one minute that a previously-available tool has disappeared from the response.
CloudWatch alarm rollback has a latency of 30–120 seconds after the alarm fires, plus the alarm's own evaluation period (typically 1–5 minutes). The total rollback window is 1.5 to 7 minutes. AliveMCP logs the entire downtime window with second-level timestamps, giving you an accurate incident duration even when AppConfig's rollback happens automatically and leaves no manual intervention trail. This matters for SLA calculations and post-mortems.
Failure modes
| Symptom | Cause | Fix |
|---|---|---|
| All tools are disabled after a flag deployment even though only one was intended to change | The values map in the new flag version omitted tools that weren't changed; AppConfig treats missing keys as removed flags, and your isToolEnabled returns false for unknown tools (fail-safe default) |
Always include all flag entries in every version of the document; do not submit partial updates; copy the full previous document and modify only the changed flag |
| Percentage rollout is not deterministic — the same client sees the tool enabled and disabled on alternate requests | Your hash function uses a non-stable identifier (e.g., request ID, timestamp) instead of a stable instance ID; each invocation gets a different hash result | Use a stable identifier: Lambda container's AWS_LAMBDA_LOG_STREAM_NAME, ECS task ARN, or a module-level UUID generated once at process start |
| CloudWatch alarm fires but AppConfig does not roll back | The alarm ARN or role ARN on the AppConfig environment monitor is incorrect; AppConfig cannot call DescribeAlarms on the alarm |
Verify the monitor role has cloudwatch:DescribeAlarms permission on the specific alarm ARN; check the AppConfig environment's monitor configuration for ARN typos |
| Feature flag profile deployment fails with a type validation error | The JSON document structure is invalid for the AWS.AppConfig.FeatureFlags type — e.g., a value in values has no corresponding entry in flags, or percentageEnabled is outside 0–100 |
Validate the document structure against the Feature Flag schema before deploying; use the AppConfig console's built-in flag editor which enforces the schema in the UI |
| Tool is enabled in the flag but still returns disabled errors after a successful deployment | Lambda containers are still running with cached flag state from before the deployment; poll interval hasn't elapsed | Wait for the poll interval to expire (15–45s depending on configuration); for urgency, force a Lambda function update to recycle containers |