Guide · AWS SSM Parameter Store
MCP Server SSM Parameter Store Hierarchy — /mcp/{env}/{service}/ naming, GetParametersByPath, IAM path policies
SSM Parameter Store is the right tool for non-secret MCP server configuration: feature flags, endpoint URLs, timeout values, poll intervals, allowed tenant lists, and runtime knobs that need to be updated without a redeployment. Unlike Secrets Manager, Parameter Store has no built-in rotation and is priced per API call (no rotation Lambda cost), making it cheaper for high-read, low-sensitivity configuration. The hierarchy design — how you structure the /-delimited path — determines whether your IAM policies are concise or sprawling, whether GetParametersByPath can load an entire service's config in one paginated call, and whether a misconfigured staging environment can accidentally read production parameters. The canonical pattern for MCP servers is /mcp/{env}/{service}/{parameter-name}.
TL;DR
Structure SSM parameters as /mcp/{env}/{service}/{name} (e.g., /mcp/prod/probe-collector/poll-interval-ms). Use GetParametersByPath with Recursive=true and handle NextToken pagination (max 10 results per page). Write IAM policies scoped to /mcp/{env}/* paths — this prevents staging MCP roles from reading production parameters. For parameters that change across environments (base URLs, feature flags), use the same relative path under each /mcp/{env}/ prefix and load them all at startup via a single GetParametersByPath call.
Hierarchy design: /mcp/{env}/{service}/{name}
A flat namespace (all parameters at the root) makes IAM scoping impossible — you can't grant read access to staging parameters without also granting production access. A hierarchy that starts with /mcp/{env}/ allows environment-scoped IAM policies and bulk-loading all parameters for a given environment in one API call.
# Recommended hierarchy for an MCP server deployment
# Pattern: /mcp/{env}/{service}/{parameter-name}
# Probe collector service — prod
/mcp/prod/probe-collector/poll-interval-ms = "60000"
/mcp/prod/probe-collector/max-concurrent-probes = "50"
/mcp/prod/probe-collector/timeout-ms = "5000"
/mcp/prod/probe-collector/registry-url = "https://mcp.so/api/v1/servers"
/mcp/prod/probe-collector/retry-count = "3"
# Same parameters for staging — identical relative paths, different env
/mcp/staging/probe-collector/poll-interval-ms = "30000" # faster in staging
/mcp/staging/probe-collector/max-concurrent-probes = "5" # lower in staging
/mcp/staging/probe-collector/timeout-ms = "10000"
/mcp/staging/probe-collector/registry-url = "https://mcp.so/api/v1/servers"
/mcp/staging/probe-collector/retry-count = "2"
# Alert router service — prod
/mcp/prod/alert-router/webhook-timeout-ms = "3000"
/mcp/prod/alert-router/max-retry-attempts = "5"
/mcp/prod/alert-router/dead-letter-queue-url = "https://sqs.us-east-1.amazonaws.com/..."
# Shared parameters (not env-specific) — use /mcp/shared/ prefix
/mcp/shared/monitoring/datadog-site = "datadoghq.com"
/mcp/shared/limits/max-tools-per-server = "100"
The /mcp/shared/ prefix is for parameters that are truly environment-agnostic. Keep this prefix small — almost everything that looks environment-agnostic today will need an environment-specific override in six months. When in doubt, put it under /mcp/{env}/.
GetParametersByPath with pagination
GetParametersByPath returns at most 10 parameters per page and always requires pagination handling. Omitting the NextToken loop silently truncates the parameter set — a production bug that is hard to detect because the service starts with partial config and fails in non-obvious ways.
// Load all parameters for a service at startup — with correct pagination
import { SSMClient, GetParametersByPathCommand } from "@aws-sdk/client-ssm";
const ssm = new SSMClient({ region: process.env.AWS_REGION });
async function loadConfig(env, service) {
const path = `/mcp/${env}/${service}/`;
const config = {};
let nextToken = undefined;
do {
const resp = await ssm.send(new GetParametersByPathCommand({
Path: path,
Recursive: true, // include sub-paths below /mcp/{env}/{service}/
WithDecryption: true, // decrypt SecureString parameters automatically
MaxResults: 10, // API max; explicit for clarity
NextToken: nextToken,
}));
for (const param of resp.Parameters ?? []) {
// Strip the path prefix to get the bare parameter name
const name = param.Name.replace(path, "");
config[name] = param.Value;
}
nextToken = resp.NextToken;
} while (nextToken);
return config;
}
// Usage at MCP server startup
const env = process.env.DEPLOYMENT_ENV ?? "prod"; // "prod" | "staging" | "dev"
const config = await loadConfig(env, "probe-collector");
// Access individual values with type conversion
const pollInterval = parseInt(config["poll-interval-ms"] ?? "60000", 10);
const maxProbes = parseInt(config["max-concurrent-probes"] ?? "50", 10);
const registryUrl = config["registry-url"];
// Validate required keys before starting (fail fast)
const REQUIRED = ["poll-interval-ms", "registry-url", "timeout-ms"];
const missing = REQUIRED.filter(k => !(k in config));
if (missing.length > 0) {
throw new Error(`Missing required SSM parameters: ${missing.join(", ")}`);
}
Path-based IAM policies
IAM resource ARNs for SSM parameters include the path, enabling least-privilege policies scoped to a specific environment or service. A staging MCP role should not be able to read production parameters — and with path-based policies, it doesn't need to.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "MCPStagingReadConfig",
"Effect": "Allow",
"Action": [
"ssm:GetParametersByPath",
"ssm:GetParameter",
"ssm:GetParameters"
],
"Resource": [
"arn:aws:ssm:us-east-1:ACCOUNT_ID:parameter/mcp/staging/*"
]
},
{
"Sid": "MCPStagingSharedRead",
"Effect": "Allow",
"Action": ["ssm:GetParametersByPath", "ssm:GetParameter"],
"Resource": [
"arn:aws:ssm:us-east-1:ACCOUNT_ID:parameter/mcp/shared/*"
]
}
]
}
# Production role — separate policy, separate role, no staging access
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "MCPProdReadConfig",
"Effect": "Allow",
"Action": [
"ssm:GetParametersByPath",
"ssm:GetParameter",
"ssm:GetParameters"
],
"Resource": [
"arn:aws:ssm:us-east-1:ACCOUNT_ID:parameter/mcp/prod/*",
"arn:aws:ssm:us-east-1:ACCOUNT_ID:parameter/mcp/shared/*"
]
}
]
}
If SecureString parameters are mixed in the hierarchy (e.g., /mcp/prod/probe-collector/db-password as SecureString), add kms:Decrypt on the KMS key ARN used to encrypt those parameters. The same GetParametersByPath call retrieves both String and SecureString parameters when WithDecryption: true is set — the KMS call is made server-side by SSM on the caller's behalf.
Caching and change propagation
MCP servers should cache SSM parameters at startup and reload periodically or on a SIGHUP signal — not on every tool call. A cache TTL of 5 minutes is typical for feature flags; 60 minutes for stable config like endpoint URLs. AWS Parameter Store has no push notification mechanism (unlike AppConfig), so polling or restart-based reload is required for dynamic config updates.
// Config cache with background refresh
class ConfigCache {
constructor(env, service, ttlMs = 5 * 60 * 1000) {
this.env = env;
this.service = service;
this.ttlMs = ttlMs;
this.cache = null;
this.lastFetched = 0;
this.refreshPromise = null;
}
async get(key, defaultValue) {
await this._ensureFresh();
return this.cache?.[key] ?? defaultValue;
}
async _ensureFresh() {
if (Date.now() - this.lastFetched < this.ttlMs && this.cache) return;
// Prevent concurrent refreshes
if (!this.refreshPromise) {
this.refreshPromise = this._refresh().finally(() => {
this.refreshPromise = null;
});
}
await this.refreshPromise;
}
async _refresh() {
this.cache = await loadConfig(this.env, this.service);
this.lastFetched = Date.now();
}
}
const configCache = new ConfigCache(env, "probe-collector");
// In MCP tool handler:
const pollInterval = parseInt(await configCache.get("poll-interval-ms", "60000"), 10);
Failure modes reference
| Failure | Symptom | Fix |
|---|---|---|
| Pagination loop omitted | MCP server loads only 10 parameters on startup; missing config causes subtle failures at runtime | Always loop on NextToken until it is undefined; log the total parameter count at startup for validation |
| Recursive=false on nested paths | GetParametersByPath returns empty list for /mcp/prod/service/ if parameters are at /mcp/prod/service/subgroup/name | Set Recursive=true unless your hierarchy is guaranteed flat at exactly one level below the path |
| IAM policy uses parameter name instead of path ARN | AccessDeniedException for GetParametersByPath even though GetParameter on individual parameters works | GetParametersByPath requires ARN with wildcard path suffix (arn:aws:ssm:...:parameter/mcp/prod/*); GetParameter requires ARN with exact name |
| WithDecryption=false for SecureString parameters | SecureString values returned as encrypted ciphertext; config loading fails on type conversion | Set WithDecryption=true; ensure IAM role has kms:Decrypt on the CMK used for SecureString parameters |
| No DEPLOYMENT_ENV variable set | MCP server loads staging config in production or vice versa | Fail fast at startup if DEPLOYMENT_ENV is missing or not in allowed set; do not fall back silently to a default environment |