AID-H-034.001: MCP Server Deployment Profile, Transport & Exposure Baseline

Version 1.20261015 · Actionable sub-technique

Classify each MCP server by deployment profile and enforce the minimum server-side exposure baseline before it is reachable by clients. This sub-technique covers coarse infrastructure-layer controls: deployment profile evidence, stdio transport hygiene, Streamable HTTP Origin validation, local binding restrictions, MCP protocol-version validation, TLS or mTLS, runtime hardening, default-deny egress, and gateway or process-level denial-of-service limits.

Pillars: infra, app

Lifecycle: scoping, building, operation

Scope boundary

Owns the coarse MCP server exposure envelope: deployment profile, stdio hygiene, HTTP Origin and protocol-version checks, local binding, TLS or mTLS, runtime hardening, default-deny egress, and gateway or process denial-of-service limits.

AID-H-034.008: AID-H-034.001 provides coarse process and gateway abuse limits; AID-H-034.008 integrates each selected MCP operation with the authoritative fine-grained budget ledger. Server exposure limits do not account for per-task, stream, tool, or protected-effect consumption.

AID-I-001: AID-H-034.001 selects and verifies the MCP server deployment baseline; AID-I-001 supplies the process, container, or WASM sandbox primitive used by that baseline. Referencing a sandbox profile is not equivalent to implementing the sandbox control.

AID-I-002: AID-H-034.001 configures server-facing transport and coarse egress posture; AID-I-002 owns independent network isolation and segmentation enforcement. Application configuration does not replace the network boundary.

Implementation guidance

AID-H-034.001-G001

Classify every MCP server into a deployment profile and require profile-specific evidence before production exposure.
Concept:

An MCP server running as a local developer process, internal team service, internet-facing API, high-value tool server, research sandbox, or gateway has a different threat model. Make that profile explicit so release reviewers can verify the right baseline instead of applying one vague checklist to every server.

Step 1: Store a profile manifest with each server

File: mcp-server-profile.json

    {
      "server_id": "payments-mcp",
      "owner": "platform-integrations",
      "profile": "high-value-tool-server",
      "transport": "streamable-http",
      "exposure": "internal",
      "privileged_tools": ["refund_payment", "read_customer_account"],
      "required_controls": [
        "origin_validation",
        "authenticated_transport",
        "tls_or_mtls",
        "non_root_container",
        "read_only_root_filesystem",
        "default_deny_egress",
        "coarse_gateway_rate_limit",
        "audit_logging"
      ],
      "cross_references": [
        "AID-I-001",
        "AID-I-002",
        "AID-D-005"
      ]
    }
    
Step 2: Enforce the profile in CI
// file: validate-mcp-profile.mjs
    import fs from "node:fs";

    const allowedProfiles = new Set([
      "local-development",
      "team-server",
      "internet-facing-api",
      "high-value-tool-server",
      "research-sandbox",
      "gateway-proxy"
    ]);

    const profile = JSON.parse(fs.readFileSync("mcp-server-profile.json", "utf8"));

    if (!allowedProfiles.has(profile.profile)) {
      throw new Error("Unknown MCP server profile: " + profile.profile);
    }

    const remoteProfiles = new Set([
      "internet-facing-api",
      "high-value-tool-server",
      "gateway-proxy"
    ]);

    if (profile.exposure !== "local" && !profile.required_controls.includes("authenticated_transport")) {
      throw new Error("Remote MCP servers require authenticated transport");
    }

    if (remoteProfiles.has(profile.profile) && !profile.required_controls.includes("tls_or_mtls")) {
      throw new Error("Internet-facing, high-value, and gateway MCP servers require TLS or mTLS");
    }

    if (remoteProfiles.has(profile.profile) && !profile.required_controls.includes("default_deny_egress")) {
      throw new Error("Internet-facing, high-value, and gateway MCP servers require default-deny egress");
    }

    if (profile.profile === "high-value-tool-server" && !profile.required_controls.includes("audit_logging")) {
      throw new Error("High-value MCP tool servers require audit logging");
    }

    console.log("MCP server profile accepted:", profile.server_id);
    

Operational notes: Use this manifest as release evidence. It should point to the exact Kubernetes manifest, gateway route, IAM policy, logging sink, and sandbox profile that implement the selected controls.

Verify safely

A separately credentialed verifier recomputes the profile from inventory facts and reads back transport, listener, identity, runtime, egress, audit, and exception state from each target.

AID-H-034.001-G002

For Streamable HTTP MCP servers, validate the Origin header, bind local servers to localhost, and require authentication on all non-local connections.
Concept:

Streamable HTTP MCP servers are reachable HTTP services. They need protocol-specific anti-DNS-rebinding controls in addition to ordinary API security. The server should reject invalid Origin headers, avoid binding local developer servers to all interfaces, and require authentication when the server is reachable beyond the local user boundary.

Step 1: Add an Origin and Host gate
// file: src/http/originGate.js
    const allowedOrigins = new Set([
      "https://assistant.example.com",
      "https://mcp-console.example.com"
    ]);

    const allowedHosts = new Set([
      "payments-mcp.internal.example.com",
      "localhost:3000",
      "127.0.0.1:3000"
    ]);

    export function enforceMcpHttpBoundary(req, res, next) {
      const origin = req.headers.origin;
      const host = req.headers.host;

      if (host && !allowedHosts.has(host)) {
        return res.status(400).json({
          jsonrpc: "2.0",
          error: { code: -32600, message: "Invalid host" }
        });
      }

      if (origin && !allowedOrigins.has(origin)) {
        return res.status(403).json({
          jsonrpc: "2.0",
          error: { code: -32600, message: "Invalid origin" }
        });
      }

      return next();
    }
    
Step 2: Bind local development servers to loopback only
// file: src/server.js
    import express from "express";
    import { enforceMcpHttpBoundary } from "./http/originGate.js";

    const app = express();
    app.use(enforceMcpHttpBoundary);

    const host = process.env.MCP_BIND_HOST || "127.0.0.1";
    // Loopback by default. A broad 0.0.0.0 bind is refused in EVERY environment
    // unless an audited opt-in is set AND authentication is mounted in front.
    if (host === "0.0.0.0") {
      if (process.env.MCP_ALLOW_PUBLIC_BIND !== "true") {
        throw new Error("MCP servers must not bind to 0.0.0.0 without MCP_ALLOW_PUBLIC_BIND=true");
      }
      if (process.env.MCP_AUTH_ENABLED !== "true") {
        throw new Error("A 0.0.0.0 bind requires authentication (MCP_AUTH_ENABLED=true)");
      }
    }

    app.listen(Number(process.env.PORT || 3000), host);
    

Operational notes: These checks do not replace OAuth or mTLS. They reduce browser-origin and local-network abuse paths, especially for developer or localhost MCP servers. The 0.0.0.0 bind is refused by default in every environment, not only outside production; a production server should bind broadly only inside a pod that already sits behind authentication (AID-H-034.002), network isolation (AID-I-002), and this Origin/Host gate.

Verify safely

A separately credentialed verifier sends approved, missing, malformed, cross-origin, DNS-rebinding Host, spoofed-forwarding, unauthenticated, and public-bind fixtures through every endpoint and independently inspects bound sockets and proxy configuration.

AID-H-034.001-G003

For stdio MCP servers, keep stdout protocol-pure, route logs to stderr, launch with a minimal environment, and avoid shell-mediated startup.
Concept:

The MCP stdio transport uses stdout for JSON-RPC messages. Any banner, log line, progress message, or crash dump written to stdout corrupts the protocol and can become an injection surface for clients that parse mixed output. Treat the subprocess boundary as a privilege boundary: minimal environment, fixed executable path, no shell, and logs only on stderr.

Example Node.js stdio server hygiene
// file: src/stdioServer.js
    import { Server } from "@modelcontextprotocol/server";
    import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";

    const originalLog = console.log;
    console.log = (...args) => {
      console.error(...args);
    };

    const server = new Server(
      { name: "payments-mcp", version: "2026.06.19" },
      { capabilities: { tools: {} } }
    );

    server.onerror = error => {
      console.error("mcp_server_error", error.message);
    };

    const transport = new StdioServerTransport();
    await server.connect(transport);

    // Deliberately never call originalLog after connect; stdout is JSON-RPC only.
    void originalLog;
    
Example safe launcher
// file: launchLocalMcp.js
    import { spawn } from "node:child_process";

    const allowedEnv = {
      PATH: "/usr/local/bin:/usr/bin:/bin",
      HOME: "/var/empty",
      NODE_ENV: "production"
    };

    const blockedEnv = [
      "LD_PRELOAD",
      "LD_LIBRARY_PATH",
      "NODE_OPTIONS",
      "PYTHONPATH",
      "PYTHONSTARTUP"
    ];

    for (const name of blockedEnv) {
      delete process.env[name];
    }

    // The privileged supervisor uses the current pinned Node runtime and drops
    // uid/gid before the MCP server executes. The child itself never runs as root.
    const child = spawn(process.execPath, ["/opt/mcp/payments-server/bin/server.js"], {
      shell: false,
      stdio: ["pipe", "pipe", "pipe"],
      env: allowedEnv,
      uid: 65534,
      gid: 65534
    });

    child.stderr.on("data", chunk => {
      process.stderr.write(chunk);
    });
    

Operational notes: Run local or stdio profiles as a non-privileged user. Do not inherit broad host environment variables or implicit credentials unless they are explicitly approved for that server profile.

Verify safely

A separately credentialed verifier replays the exact launch spec, injects hostile environment and shell metacharacters, captures byte-exact stdout/stderr, parses every stdout frame, and confirms the child identity and open-descriptor set.

AID-H-034.001-G004

Pin Streamable HTTP to MCP 2026-07-28 and reject protocol, per-request metadata, or routing-header/body disagreement before dispatch.
Concept

This is the exact MCP v2 2026-07-28 profile. It has no initialize/notifications/initialized lifecycle and no Mcp-Session-Id. Every Streamable HTTP JSON-RPC request is sent by POST with the protocol version and client capabilities in required per-request metadata. Clients SHOULD include clientInfo on every request unless specifically configured not to; when present, it must contain a valid name and version and remains self-reported display, logging, and debugging metadata that must not drive authentication, authorization, or other security decisions. MCP-Protocol-Version, Mcp-Method, and applicable Mcp-Name values must agree with the body before dispatch. Header names are case-insensitive; values are case-sensitive.

Fail-closed envelope gate
// File: src/http/mcp20260728EnvelopeGate.js
const VERSION = "2026-07-28";
const META = Object.freeze({
  version: "io.modelcontextprotocol/protocolVersion",
  clientInfo: "io.modelcontextprotocol/clientInfo",
  clientCapabilities: "io.modelcontextprotocol/clientCapabilities"
});
const NAMED = new Map([
  ["tools/call", "name"], ["resources/read", "uri"], ["prompts/get", "name"],
  ["tasks/get", "taskId"], ["tasks/update", "taskId"], ["tasks/cancel", "taskId"]
]);

function reject(res, id, status, code, message, data) {
  return res.status(status).json({
    jsonrpc: "2.0", id: id ?? null,
    error: { code, message, ...(data === undefined ? {} : { data }) }
  });
}
function decodeHeader(value) {
  if (typeof value !== "string") throw new Error("missing_header");
  if (value.startsWith("=?base64?") || value.endsWith("?=")) {
    const match = value.match(/^=\?base64\?([A-Za-z0-9+/]+={0,2})\?=$/);
    if (!match) throw new Error("malformed_base64_sentinel");
    const decoded = Buffer.from(match[1], "base64");
    if (decoded.toString("base64") !== match[1] ||
        decoded.toString("utf8").includes("\uFFFD")) {
      throw new Error("noncanonical_or_invalid_utf8_header");
    }
    return decoded.toString("utf8");
  }
  if (!/^[\x20-\x7E]+$/.test(value) || value !== value.trim()) {
    throw new Error("unsafe_plain_header");
  }
  return value;
}

export function enforceMcp20260728Envelope(req, res, next) {
  const body = req.body;
  if (!body || body.jsonrpc !== "2.0" || typeof body.method !== "string") {
    return reject(res, body?.id, 400, -32600, "Invalid JSON-RPC request");
  }
  if (["initialize", "notifications/initialized"].includes(body.method)) {
    return reject(res, body.id, 404, -32601, "Method not found");
  }
  if (req.get("Mcp-Session-Id") !== undefined ||
      req.get("Last-Event-ID") !== undefined) {
    return reject(res, body.id, 400, -32600,
      "Session or resumability state is not permitted by this profile");
  }
  const meta = body.params?._meta;
  const headerVersion = req.get("MCP-Protocol-Version");
  const bodyVersion = meta?.[META.version];
  if (typeof headerVersion !== "string" || typeof bodyVersion !== "string" ||
      headerVersion !== bodyVersion) {
    return reject(res, body.id, 400, -32020,
      "MCP-Protocol-Version header/body mismatch");
  }
  if (headerVersion !== VERSION) {
    return reject(res, body.id, 400, -32022,
      "Unsupported protocol version", { supported: [VERSION], requested: headerVersion });
  }
  const clientInfo = meta?.[META.clientInfo];
  const clientCapabilities = meta?.[META.clientCapabilities];
  const invalidClientInfo = clientInfo !== undefined && (
    !clientInfo || Object.getPrototypeOf(clientInfo) !== Object.prototype ||
    typeof clientInfo.name !== "string" || clientInfo.name.length === 0 ||
    typeof clientInfo.version !== "string" || clientInfo.version.length === 0
  );
  if (invalidClientInfo || !clientCapabilities ||
      Object.getPrototypeOf(clientCapabilities) !== Object.prototype) {
    return reject(res, body.id, 400, -32602,
      "Invalid or missing required per-request metadata");
  }
  if (req.get("Mcp-Method") !== body.method) {
    return reject(res, body.id, 400, -32020, "Mcp-Method header/body mismatch");
  }
  const field = NAMED.get(body.method);
  if (field) {
    try {
      if (typeof body.params?.[field] !== "string" ||
          decodeHeader(req.get("Mcp-Name")) !== body.params[field]) {
        return reject(res, body.id, 400, -32020,
          "Mcp-Name header/body mismatch");
      }
    } catch {
      return reject(res, body.id, 400, -32020,
        "Missing or malformed Mcp-Name header");
    }
  }
  req.mcpContext = Object.freeze({
    protocolVersion: VERSION,
    clientInfo: clientInfo === undefined ? null : Object.freeze({ ...clientInfo }),
    clientCapabilities: Object.freeze({ ...clientCapabilities })
  });
  return next();
}

Action: mount bounded JSON parsing and Origin/authentication checks before this gate and method dispatch after it. An unimplemented method returns HTTP 404 with JSON-RPC -32601. Negative fixtures must reject forbidden handshake methods and session/resume headers as shown before dispatch. Validate every x-mcp-header parameter separately with the same decoding and body comparison.

Verify safely

Run exact 2026-07-28 fixtures for missing, malformed, and mismatched headers; Base64 sentinel names; unsupported version -32022; header mismatch -32020; unimplemented method 404/-32601; per-request metadata including omitted optional client information and malformed present client information; exact {supported, requested} unsupported-version data; Tasks routing names; handshake/session rejection; and valid server/discover. Confirm only the MCP v2 2026-07-28 dispatcher and its explicit-handle state store are reachable.

AID-H-034.001-G005

Require TLS for remote MCP servers and use mTLS for high-value internal tool servers or service-to-service MCP paths.
Concept:

Streamable HTTP MCP servers are ordinary HTTP services with unusual authority: they expose tools, resources, prompts, tasks, and server-to-client notifications. Use TLS for every remote deployment, and require mTLS when the server is reachable from internal service meshes, agent gateways, or high-value automation clients where client workload identity matters.

Example Envoy TLS and client certificate requirement
# file: envoy/mcp-listener-mtls.yaml
    transport_socket:
      name: envoy.transport_sockets.tls
      typed_config:
        "@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext
        common_tls_context:
          tls_params:
            tls_minimum_protocol_version: TLSv1_2
            tls_maximum_protocol_version: TLSv1_3
          tls_certificates:
            - certificate_chain:
                filename: /etc/envoy/certs/server.crt
              private_key:
                filename: /etc/envoy/certs/server.key
          validation_context:
            trusted_ca:
              filename: /etc/envoy/certs/client-ca.crt
        require_client_certificate: true
    
Extract identity from the authorized TLS socket
// File: src/http/clientCertIdentity.js
function verifiedSpiffeIdentity(socket, policy) {
  if (!socket.authorized) throw new Error("mTLS_peer_not_authorized:" + socket.authorizationError);
  const certificate = socket.getPeerCertificate(true);
  if (!certificate?.raw || !certificate.fingerprint256 || !certificate.subjectaltname) {
    throw new Error("mTLS_peer_certificate_missing_required_identity");
  }
  const identities = certificate.subjectaltname.split(/,\s*/)
    .filter(value => value.startsWith("URI:spiffe://"))
    .map(value => value.slice(4));
  if (identities.length !== 1) throw new Error("mTLS_peer_requires_one_SPIFFE_URI_SAN");
  const identity = new URL(identities[0]);
  if (identity.protocol !== "spiffe:" ||
      !policy.allowedSpiffeTrustDomains.includes(identity.hostname) ||
      (policy.allowedWorkloadIds?.length && !policy.allowedWorkloadIds.includes(identity.href))) {
    throw new Error("mTLS_peer_workload_identity_outside_policy");
  }
  return Object.freeze({
    spiffeId: identity.href,
    certificateFingerprint256: certificate.fingerprint256,
    certificateValidTo: certificate.valid_to,
    policyVersion: policy.version
  });
}

export function requireClientCertificateIdentity(policy) {
  if (!policy?.version || !Array.isArray(policy.allowedSpiffeTrustDomains) ||
      !policy.allowedSpiffeTrustDomains.length) throw new Error("invalid_signed_mTLS_identity_policy");
  return function clientCertificateGate(req, res, next) {
    try {
      req.mcpClientWorkload = verifiedSpiffeIdentity(req.socket, policy);
      return next();
    } catch {
      return res.status(401).end();
    }
  };
}

Operational notes: Enforce a TLS floor of 1.2 or higher (prefer 1.3) with a modern cipher allowlist; a Node server terminating TLS directly should set https.createServer({ minVersion: "TLSv1.2", requestCert: true, rejectUnauthorized: true }). When Envoy terminates mTLS, perform this identity check in a trusted ext_authz filter or pass a sanitized identity only over an authenticated internal hop; never trust caller-supplied X-Forwarded-Client-Cert. TLS and mTLS authenticate the transport and workload channel; they do not replace OAuth token audience validation, per-tool authorization, or object-level access checks.

Verify safely

A separately credentialed verifier performs independent TLS scans and connection tests for valid, missing, expired, revoked, wrong-name, wrong-SPIFFE, untrusted-CA, downgraded, and spoofed-forwarded-certificate cases.

AID-H-034.001-G006

Apply a hardened runtime baseline for production MCP servers, including non-root containers, read-only filesystems, dropped Linux capabilities, default-deny egress, and process-level resource limits.
Concept:

Many MCP servers bridge directly to filesystems, APIs, databases, browsers, shell tools, or internal networks. A handler bug should not automatically become a host compromise. Put the server in a constrained runtime before exposing tools, and default-deny outbound network access except for explicitly required upstreams.

Example Kubernetes security context
# file: k8s/payments-mcp-deployment.yaml
    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: payments-mcp
    spec:
      replicas: 2
      selector:
        matchLabels:
          app: payments-mcp
      template:
        metadata:
          labels:
            app: payments-mcp
        spec:
          automountServiceAccountToken: false
          securityContext:
            runAsNonRoot: true
            seccompProfile:
              type: RuntimeDefault
          containers:
            - name: server
              image: registry.example.com/payments-mcp:2026.06.10
              ports:
                - containerPort: 3000
              resources:
                requests:
                  cpu: "250m"
                  memory: "256Mi"
                limits:
                  cpu: "1"
                  memory: "512Mi"
              securityContext:
                allowPrivilegeEscalation: false
                readOnlyRootFilesystem: true
                capabilities:
                  drop: ["ALL"]
---
    apiVersion: networking.k8s.io/v1
    kind: NetworkPolicy
    metadata:
      name: payments-mcp-default-deny-egress
    spec:
      podSelector:
        matchLabels:
          app: payments-mcp
      policyTypes: ["Egress"]
      egress: []
    

Operational notes: Keep this as the server baseline. Add narrow egress policies only for approved upstream APIs. For tools that execute untrusted code, launch browsers, run subprocesses, or inspect unknown artifacts, call AID-I-001 sandbox profiles instead of running inside the main server process.

Verify safely

A separately credentialed verifier reads back effective admission/runtime/network state from every replica and executes escape, privilege, write, service-account, egress, fork, and resource-exhaustion tests in a release-equivalent environment.

AID-H-034.001-G007

Enforce coarse gateway or process-level denial-of-service limits for the MCP endpoint before applying fine-grained per-principal budgets.
Concept:

This control is the broad front-door safety net: request size, concurrent connections, stream lifetime, process CPU, process memory, and global request rate. Per-user, per-client, per-tenant, per-tool, per-subscription, per-sampling, per-task, and backend-effect budgets belong in AID-H-034.008.

Example Envoy route limits
# file: envoy/mcp-route.yaml
    max_request_headers_kb: 32
    common_http_protocol_options:
      max_connection_duration: 3600s
      idle_timeout: 300s
    request_headers_timeout: 10s
    stream_idle_timeout: 120s
    typed_per_filter_config:
      envoy.filters.http.local_ratelimit:
        "@type": type.googleapis.com/udpa.type.v1.TypedStruct
        type_url: type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit
        value:
          stat_prefix: payments_mcp_local_rate_limit
          token_bucket:
            max_tokens: 300
            tokens_per_fill: 300
            fill_interval: 60s
          filter_enabled:
            runtime_key: local_rate_limit_enabled
            default_value:
              numerator: 100
              denominator: HUNDRED
          filter_enforced:
            runtime_key: local_rate_limit_enforced
            default_value:
              numerator: 100
              denominator: HUNDRED
    
Example Express body cap
// file: src/server.js
    import express from "express";

    const app = express();
    app.use(express.json({ limit: "1mb", strict: true }));
    

Operational notes: A WAF or API gateway is acceptable as one layer for internet-facing MCP servers, but it is not a substitute for protocol-aware authorization, tool validation, elicitation safety, and per-principal budgets inside the server.

Verify safely

A separately credentialed verifier runs distributed load, slow-header/body, oversized, decompression, connection-hoarding, queue-saturation, timeout, restart, and failover tests while reconciling accepted, rejected, timed-out, and completed work.

Open-source tools

Commercial / hosted tools

Threat mappings

MITRE ATLAS

MAESTRO

OWASP LLM Top 10 2026

OWASP ML Top 10 2023

OWASP Top 10 for Agentic Applications 2026

NIST Adversarial Machine Learning 2025

Cisco Integrated AI Security and Safety Framework

Google Secure AI Framework 2.0 - Risks

Databricks AI Security Framework 3.0

AID-H-034.001: MCP Server Deployment Profile, Transport & Exposure Baseline | AIDEFEND

Loading interactive view…

A Practical Knowledge Base for AI Security Defenses

Version