AID-H-034.001: MCP Server Deployment Profile, Transport & Exposure Baseline
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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
AID-H-034.001-G001
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
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
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
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
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
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
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
- Model Context Protocol TypeScript SDK v2 (@modelcontextprotocol/server and @modelcontextprotocol/client)
- Model Context Protocol Python SDK (mcp)
- Envoy
- Open Policy Agent (OPA)
- Kubernetes NetworkPolicy
- Falco
Commercial / hosted tools
- Cloudflare Web Application Firewall (WAF)
- Akamai App & API Protector
- Kong Konnect
- Datadog Cloud SIEM
Threat mappings
MITRE ATLAS
- AML.T0049 Exploit Public-Facing Application
- AML.T0105 Escape to Host
- AML.T0034.002 Cost Harvesting: Agentic Resource Consumption
- AML.T0072 Cyber Communication Channel (default-deny server egress denies callback channels from a compromised MCP server)
- AML.T0122 Exploitation of Remote Services (local binding, Origin validation, and mutual TLS shrink the reachable MCP server surface)
- AML.T0132 Misconfigured or Publicly Exposed AI Services (MCP deployment gates reject unsafe public binds and unauthenticated remote exposure)
MAESTRO
- Integration Risks (L7)
OWASP LLM Top 10 2026
- LLM06:2026 Unbounded Consumption (coarse gateway and process limits bound MCP-driven request floods, while per-task and per-tool budgets are enforced separately)
OWASP ML Top 10 2023
- N/A
OWASP Top 10 for Agentic Applications 2026
- ASI07:2026 Insecure Inter-Agent Communication
- ASI08:2026 Cascading Failures
NIST Adversarial Machine Learning 2025
- N/A
Cisco Integrated AI Security and Safety Framework
- AISubtech-13.1.1 Compute Exhaustion
- AISubtech-13.1.2 Memory Flooding
- AISubtech-13.1.4 Application Denial of Service
- AISubtech-13.2.1 Service Misuse for Cost Inflation (bounds coarse MCP endpoint consumption; fine-grained budgets remain separate)
- AISubtech-14.1.2 Insufficient Access Controls (hardens the actual MCP hosting runtime and verifies effective privileges)
- AISubtech-4.3.1 Schema Inconsistencies
- AISubtech-4.3.3 Server Rebinding Attack
- AISubtech-4.3.6 Cross-Origin Exploitation
- AISubtech-8.2.3 Data Exfiltration via Agent Tooling
- AITech-13.1 Disruption of Availability
- AITech-13.2 Cost Harvesting / Repurposing (bounds coarse MCP endpoint consumption; fine-grained budgets remain separate)
- AITech-14.1 Unauthorized Access (hardens the actual MCP hosting runtime and verifies effective privileges)
- AITech-16.1 Eavesdropping
- AITech-4.3 Protocol Manipulation
- AITech-8.2 Data Exfiltration / Exposure
Google Secure AI Framework 2.0 - Risks
- IIC: Insecure Integrated Component
- DMS: Denial of ML Service
Databricks AI Security Framework 3.0
- Agents - Tools MCP Server 13.20: Insecure Server Configuration
- Agents - Tools MCP Server 13.25: Insecure Communication