Guide · AWS CloudFront

MCP Server CloudFront Functions — URL normalization, security headers, A/B routing at edge

CloudFront Functions run lightweight JavaScript at the CloudFront edge before a request reaches the cache or the origin — and before a response reaches the client — with sub-millisecond execution time and no cold start. They are not Lambda@Edge: they run in a restricted JavaScript runtime (ES5.3 + limited ES6), have a 2ms CPU time limit per invocation, cannot make network calls, and cannot access environment variables. But for the use cases they cover — URL normalization, query string sorting, security header injection, and simple request routing — they are faster and cheaper than Lambda@Edge. For MCP server deployments, the canonical use cases are: canonicalizing /seo/mcp-server-foo/ to /seo/mcp-server-foo (trailing slash removal for SEO), normalizing query parameter order for better cache hit rates, and injecting security response headers.

TL;DR

Use CloudFront Functions (not Lambda@Edge) for stateless, sub-2ms operations: URL normalization, query param sorting, security headers, simple redirects. Use Lambda@Edge when you need network access (DynamoDB lookup, Secrets Manager), environment variables, Node.js modules, or more than 2ms of CPU. Deploy Functions as viewer-request (to normalize the cache key before CloudFront checks the cache) or viewer-response (to add security headers to every response). Functions deployed at viewer-request run on every request including cache hits — keep them fast. A trailing slash normalizer at viewer-request is the single highest-value CloudFront Function for MCP server SEO pages.

CloudFront Functions vs Lambda@Edge: decision guide

RequirementCloudFront FunctionsLambda@Edge
CPU time limit2ms per invocation30s (viewer) / 30s (origin)
Memory limit2 MB128 MB (viewer), 10 GB (origin-request)
Network callsNot supportedFull Node.js HTTP/HTTPS, AWS SDK
Environment variablesNot supportedSupported (origin-request/origin-response)
RuntimeCloudFront-restricted JS (ES5.3 + some ES6)Node.js 18.x / 20.x, Python 3.12
npm modulesNot supportedFull npm support
Execution eventsviewer-request, viewer-responseviewer-request, origin-request, origin-response, viewer-response
Pricing (per 1M invocations)$0.10$0.60 (viewer) / $0.60 (origin)
Cold startNone — always warmYes (first invocation in a PoP)
Deployment regionGlobal (deploy once, runs everywhere)Must be us-east-1; replicates globally

URL normalization: trailing slash removal for SEO canonicalization

MCP server SEO pages at /seo/mcp-server-foo and /seo/mcp-server-foo/ are treated as different URLs by search engines — unless CloudFront redirects one to the other. A viewer-request Function that removes trailing slashes (except for the root /) ensures all traffic hits the canonical URL.

// CloudFront Function — viewer-request event
// Removes trailing slashes and normalizes query parameter order for better cache hit rate

function handler(event) {
  var request = event.request;
  var uri = request.uri;

  // 1. Remove trailing slash (except root path /)
  if (uri !== "/" && uri.endsWith("/")) {
    var qs = "";
    // Preserve query string in redirect
    if (request.querystring) {
      var params = [];
      for (var key in request.querystring) {
        var val = request.querystring[key];
        if (val.multiValue) {
          for (var i = 0; i < val.multiValue.length; i++) {
            params.push(key + "=" + encodeURIComponent(val.multiValue[i].value));
          }
        } else {
          params.push(key + "=" + encodeURIComponent(val.value));
        }
      }
      // Sort params for consistent cache key
      params.sort();
      if (params.length > 0) qs = "?" + params.join("&");
    }

    return {
      statusCode: 301,
      statusDescription: "Moved Permanently",
      headers: {
        location: { value: uri.slice(0, -1) + qs },
        "cache-control": { value: "public, max-age=31536000" }, // cache the redirect
      },
    };
  }

  // 2. Normalize query string order (sort params alphabetically for better cache hit rate)
  // This runs on non-redirect requests (no trailing slash)
  if (request.querystring) {
    var sortedParams = [];
    var keys = Object.keys(request.querystring).sort();
    for (var k = 0; k < keys.length; k++) {
      var key = keys[k];
      sortedParams.push({ name: key, value: request.querystring[key] });
    }
    // Rebuild querystring object in sorted order
    var newQs = {};
    for (var j = 0; j < sortedParams.length; j++) {
      newQs[sortedParams[j].name] = sortedParams[j].value;
    }
    request.querystring = newQs;
  }

  return request;
}

Deploy this Function at the viewer-request event on all cache behaviors that serve SEO pages. The Function runs on every request — including cache hits — before CloudFront checks its cache, so the normalized URL is always used as the cache key.

Security header injection at viewer-response

Security response headers (HSTS, CSP, X-Frame-Options) must be present on every response — including responses served from CloudFront's edge cache. A viewer-response Function is the right place to add these headers because it runs on both cache hits and cache misses, without the complexity and cost of Lambda@Edge.

// CloudFront Function — viewer-response event
// Injects security headers into every response

function handler(event) {
  var response = event.response;
  var headers = response.headers;

  // Strict-Transport-Security: 1 year, includeSubDomains, preload
  headers["strict-transport-security"] = {
    value: "max-age=31536000; includeSubDomains; preload"
  };

  // Content-Security-Policy — adjust for MCP server's actual resource origins
  headers["content-security-policy"] = {
    value: [
      "default-src 'self'",
      "script-src 'self' 'unsafe-inline'",  // remove unsafe-inline once you have hashes
      "style-src 'self' 'unsafe-inline'",
      "img-src 'self' data: https:",
      "connect-src 'self' https://alivemcp.com",
      "frame-ancestors 'none'",
      "base-uri 'self'",
      "form-action 'self'",
    ].join("; ")
  };

  // Prevent MIME type sniffing
  headers["x-content-type-options"] = { value: "nosniff" };

  // Clickjacking protection (redundant with frame-ancestors CSP but belt-and-suspenders)
  headers["x-frame-options"] = { value: "DENY" };

  // Referrer policy — don't leak paths in cross-origin requests
  headers["referrer-policy"] = { value: "strict-origin-when-cross-origin" };

  // Permissions policy — disable features not used by MCP server
  headers["permissions-policy"] = {
    value: "camera=(), microphone=(), geolocation=(), payment=()"
  };

  return response;
}

// Note: viewer-response Functions cannot modify the status code
// If you need conditional redirects, use viewer-request instead

A/B test routing: weighted cookie-based split

For MCP server landing page experiments (new pricing copy, hero messaging, CTA color), a viewer-request Function can implement a weighted A/B split without Lambda@Edge or any origin-side logic. The Function assigns a variant cookie on first visit and routes to different origin paths based on the cookie.

// CloudFront Function — viewer-request
// A/B routing: 50% to /landing-v2/, 50% to / (control)

function handler(event) {
  var request = event.request;
  var cookies = request.headers.cookie ? parseCookies(request.headers.cookie.value) : {};

  // Only A/B test the root path
  if (request.uri !== "/" && !request.uri.startsWith("/?")) {
    return request;
  }

  var variant = cookies["ab-variant"];

  if (!variant) {
    // Assign variant — CloudFront Functions cannot set cookies on request events
    // Instead, redirect to a path that sets the cookie (origin-side), or
    // encode variant in a query param for the origin to handle
    // Simple approach: route based on viewer country or random URI-encoded decision
    // Math.random() is available in CloudFront Functions runtime
    variant = Math.random() < 0.5 ? "control" : "treatment";
  }

  if (variant === "treatment") {
    // Rewrite URI to treatment landing page
    request.uri = "/landing-v2/";
    // Add custom header to let origin distinguish (for analytics)
    request.headers["x-ab-variant"] = { value: "treatment" };
  }

  return request;
}

function parseCookies(cookieHeader) {
  var cookies = {};
  if (!cookieHeader) return cookies;
  cookieHeader.split(";").forEach(function(cookie) {
    var parts = cookie.split("=");
    var name = parts[0].trim();
    var value = parts.slice(1).join("=").trim();
    cookies[name] = value;
  });
  return cookies;
}

// Important: CloudFront Functions cannot set cookies in viewer-request responses.
// To persist the variant assignment, use the origin to set the cookie on the response,
// or use a viewer-response Function combined with a variant header from the origin.

Deploying and testing CloudFront Functions

CloudFront Functions have a development/live stage system — you deploy to DEVELOPMENT, test locally, then promote to LIVE. Unlike Lambda@Edge, there is no lambda:GetFunction or IAM execution role required: Functions are managed entirely within the CloudFront service.

# Create and deploy a CloudFront Function
aws cloudfront create-function \
  --name mcp-url-normalizer \
  --function-config '{"Comment": "Trailing slash removal and query normalization", "Runtime": "cloudfront-js-2.0"}' \
  --function-code fileb://normalizer.js
# Returns: FunctionARN and ETag

# Test the function with a synthetic event (in DEVELOPMENT stage)
aws cloudfront test-function \
  --name mcp-url-normalizer \
  --stage DEVELOPMENT \
  --if-match EXXXXXXXXXXXX \
  --event-object '{
    "version": "1.0",
    "context": { "eventType": "viewer-request" },
    "viewer": { "ip": "1.2.3.4" },
    "request": {
      "method": "GET",
      "uri": "/seo/mcp-server-cloudfront/",
      "querystring": {},
      "headers": {},
      "cookies": {}
    }
  }'
# Response shows the modified request object and any console.log output

# Publish to LIVE stage (required before attaching to distribution)
aws cloudfront publish-function \
  --name mcp-url-normalizer \
  --if-match EXXXXXXXXXXXX

# Attach to distribution cache behavior (update-distribution required)
# In the CacheBehavior for the SEO paths:
# FunctionAssociations:
#   Items:
#     - FunctionARN: arn:aws:cloudfront::ACCOUNT_ID:function/mcp-url-normalizer
#       EventType: viewer-request

Failure modes reference

FailureSymptomFix
Function exceeds 2ms CPU limitCloudFront returns 503 with x-cache: Error from cloudfront; Function killed mid-executionTest with cloudfront test-function before deploying; profile with console.log timing; remove loops over large datasets
ES6+ syntax unsupported in cloudfront-js-1.0 runtimeSyntaxError on function creation; arrow functions, const/let, template literals failUse cloudfront-js-2.0 runtime (supports most ES2020) or transpile to ES5 before upload
Function not promoted to LIVE stageFunction appears to exist but distribution update fails with "function not in LIVE stage"Run publish-function command before attaching to distribution; DEVELOPMENT stage is test-only
Viewer-response Function cannot change status codeJavaScript error or silent 500 when Function attempts response.statusCode = 301Use viewer-request for redirects (return response object with statusCode); viewer-response can only modify headers
Security headers applied to SSE/WebSocket pathsSSE Connection breaks if Content-Security-Policy blocks the event stream; WebSocket 403Attach viewer-response Function only to cache behaviors serving HTML pages; skip SSE/WebSocket/API behaviors
console.log output not visible in productionDebugging in LIVE stage is blind; Function silently failsUse test-function in DEVELOPMENT stage for all debugging; enable CloudFront standard logging to S3 for production traces (logs don't include Function output but show status codes)