Guide · AWS DevOps

MCP Server Parameter Store — AWS SSM Parameter Store hierarchical config, SecureString, GetParametersByPath

AWS Systems Manager Parameter Store is the standard way to store MCP server configuration (non-secret environment-specific values) and inject them into ECS tasks or read them at application startup. Three Parameter Store patterns cause consistent confusion for MCP server teams: the GetParameters (plural) vs GetParameter (singular) IAM trap (when ECS injects secrets from SSM into a task definition using the secrets array, the ECS agent calls ssm:GetParameters — plural — not ssm:GetParameter; granting only ssm:GetParameter to the execution role leaves the task failing to start with "AccessDeniedException" on the ECS console), Standard vs Advanced tier selection (Standard is free and covers most MCP server use cases — 10,000 parameters/region, 10KB limit, no parameter policies; Advanced adds TTL-based expiration, parameter policies, and higher limits at $0.05/parameter/month — use Standard unless you specifically need parameter expiration), and API call costs on hot paths (GetParameter and GetParametersByPath are billed at $0.05 per 10,000 calls for Standard tier — calling SSM per-request inside a hot MCP tool handler will accumulate costs and add 5-20ms of latency per call; cache parameters at startup and refresh on a timer, not per-request).

TL;DR

Grant ssm:GetParameters (plural) on the ECS execution role for task injection. Use /myapp/env/key hierarchy and GetParametersByPath to load all config in one call at startup. Cache the values in-process — never call SSM per-request. Use SecureString for anything sensitive; the free aws/ssm KMS key covers most cases.

Parameter hierarchy and ECS injection

// AWS CLI: create parameters with hierarchy
aws ssm put-parameter \
  --name "/mcp-server/prod/DATABASE_URL" \
  --value "postgresql://user:pass@host:5432/db" \
  --type SecureString   # encrypted with aws/ssm KMS key (free)

aws ssm put-parameter \
  --name "/mcp-server/prod/QUEUE_URL" \
  --value "https://sqs.us-east-1.amazonaws.com/123456789/mcp-jobs" \
  --type String

aws ssm put-parameter \
  --name "/mcp-server/prod/LOG_LEVEL" \
  --value "info" \
  --type String
// CDK: ECS task definition with SSM parameter injection
import * as ssm from "aws-cdk-lib/aws-ssm";

// Reference existing SSM parameters (created outside CDK or by another stack)
const dbUrl = ssm.StringParameter.fromSecureStringParameterAttributes(this, "DbUrl", {
  parameterName: "/mcp-server/prod/DATABASE_URL",
  version: 1,   // pin to a specific version; omit for "latest"
});

const queueUrl = ssm.StringParameter.valueForStringParameter(
  this, "/mcp-server/prod/QUEUE_URL"
);

// Inject into ECS container — ECS agent fetches at task start, not at synth time
const container = taskDef.addContainer("app", {
  image: ecs.ContainerImage.fromEcrRepository(repository, imageTag),
  secrets: {
    DATABASE_URL: ecs.Secret.fromSsmParameter(dbUrl),
    // ^ Execution role automatically granted ssm:GetParameters on this ARN
  },
  environment: {
    // Non-secret values can go in environment directly
    QUEUE_URL: queueUrl,    // resolved at synth time via SSM lookup
    LOG_LEVEL: "info",
  },
});

// IAM: execution role needs GetParameters (plural) for all injected params
// CDK's ecs.Secret.fromSsmParameter() handles this automatically.
// If using raw task definition JSON (not CDK), add this manually:
executionRole.addToPolicy(new iam.PolicyStatement({
  actions: ["ssm:GetParameters"],   // PLURAL — ECS uses batch API
  resources: [
    `arn:aws:ssm:${this.region}:${this.account}:parameter/mcp-server/prod/*`,
  ],
}));

Loading all config at startup with GetParametersByPath

For MCP servers that need many configuration values, GetParametersByPath loads all parameters under a path prefix in a single (paginated) API call at application startup — far more efficient than individual GetParameter calls.

import { SSMClient, GetParametersByPathCommand } from "@aws-sdk/client-ssm";

const ssm = new SSMClient({ region: process.env.AWS_REGION ?? "us-east-1" });

interface Config {
  DATABASE_URL: string;
  QUEUE_URL: string;
  LOG_LEVEL: string;
}

async function loadConfig(path: string): Promise {
  const params: Record = {};
  let nextToken: string | undefined;

  // GetParametersByPath returns max 10 per call — paginate with NextToken
  do {
    const response = await ssm.send(new GetParametersByPathCommand({
      Path: path,           // e.g. "/mcp-server/prod/"
      Recursive: true,      // include sub-paths
      WithDecryption: true, // decrypt SecureString values
      MaxResults: 10,       // max allowed per call
      NextToken: nextToken,
    }));

    for (const param of response.Parameters ?? []) {
      if (!param.Name || !param.Value) continue;
      // Strip the path prefix to get the env var name
      // "/mcp-server/prod/DATABASE_URL" -> "DATABASE_URL"
      const key = param.Name.replace(`${path}/`, "");
      params[key] = param.Value;
    }

    nextToken = response.NextToken;
  } while (nextToken);

  // Validate required keys are present
  const required = ["DATABASE_URL", "QUEUE_URL", "LOG_LEVEL"] as const;
  for (const key of required) {
    if (!params[key]) throw new Error(`Missing required config: ${key}`);
  }

  return params as unknown as Config;
}

// Call once at startup — cache for the lifetime of the process
// For long-running processes: refresh on a timer (e.g., every 5 minutes)
const config = await loadConfig("/mcp-server/prod");
console.log(JSON.stringify({ level: "INFO", event: "config.loaded", keyCount: Object.keys(config).length }));

For ECS deployments, prefer injecting via the task definition secrets array over calling SSM at runtime — the ECS agent handles the API calls during task startup, and you don't need SSM IAM permissions on the task role. Runtime SSM calls are more appropriate for parameters that change frequently (feature flags, runtime thresholds) and need to be refreshed without a task restart.

Tier selection and cost

FeatureStandard (free)Advanced ($0.05/param/month)
Max parameters per region10,000100,000
Max value size4 KB (String/StringList) / 4 KB (SecureString)8 KB
Throughput40 TPS (GetParameter) / 100 TPS (PutParameter)1,000 TPS (GetParameter higher-throughput add-on)
Parameter policies (TTL, expiration, notifications)NoYes
API call costFree for first 10,000 calls/month; $0.05/10,000 afterSame API cost; parameter storage is $0.05/month/parameter

Standard tier is correct for MCP servers with <10,000 config parameters. Upgrade to Advanced only if you need parameter policies (e.g., auto-expiring API keys with TTL notifications via EventBridge) or value sizes over 4 KB. Switching an existing parameter from Standard to Advanced requires deleting and recreating it — the tier cannot be downgraded.

Common failure modes

SymptomCauseFix
ECS task fails to start: "AccessDeniedException" on SSMExecution role has ssm:GetParameter (singular) not ssm:GetParameters (plural) — ECS uses the batch APIAdd ssm:GetParameters (plural) to the execution role on the parameter ARN(s)
Parameter injection works for String, fails for SecureStringExecution role missing kms:Decrypt on the KMS key used for SecureStringGrant kms:Decrypt on the KMS key ARN (for aws/ssm key: arn:aws:kms:REGION:ACCOUNT:alias/aws/ssm)
GetParametersByPath returns only 10 parameters, others missingAPI returns max 10 per call; not paginating with NextTokenLoop on NextToken until undefined
SSM calls adding 20ms+ latency to every MCP tool callCalling GetParameter inside the request handler on a hot pathLoad all parameters at startup and cache; refresh on a 5-minute timer for TTL-sensitive values
Updated parameter value not picked up by running ECS tasksECS injects SSM values at task start — running tasks see the old valueForce a new ECS deployment after updating parameters: aws ecs update-service --force-new-deployment
"ParameterAlreadyExists" when running PUT in CITrying to create a parameter that already exists without --overwriteAdd --overwrite flag to aws ssm put-parameter for CI idempotency
Parameter not found in CDK synth: "resolve" errorUsing ssm.StringParameter.valueForStringParameter() on a parameter that doesn't exist yetCreate the parameter before running cdk synth, or use CDK's fromStringParameterAttributes which defers resolution to deploy time