Skip to content

MCP configuration

McpConfig and its NESTRS_MCP__* keys, plus how a failing operation answers a language model without leaking.

McpConfig carries the streamable-HTTP options, dual-path like every nest-rs-* config: NESTRS_MCP__* env vars over an optional pinned base. Importing McpModule is what resolves it; without the import every mount runs on the defaults.

apps/api/src/module.rs
#[module(imports = [
HttpModule::for_root(None),
McpModule::for_root(McpConfig::default().with_allowed_hosts(["mcp.example.com"])),
])]
pub struct AssistantModule;

Pinning options and declaring the app’s identity is the same one call — an McpConfig converts into an McpOptions whose server is empty, so write the whole value out when you need both:

apps/api/src/module.rs
McpModule::for_root(McpOptions {
config: Some(McpConfig::default().with_allowed_hosts(["mcp.example.com"])),
server: Some(McpIdentity::new("acme-assistant", env!("CARGO_PKG_VERSION"))),
}),
VariableDefaultWhat it controls
NESTRS_MCP__ALLOWED_HOSTSlocalhost,127.0.0.1,::1Host header allowlist — anti-DNS-rebinding
NESTRS_MCP__LEGACY_SESSION_MODEtrueKeep sessions for protocol versions before 2026-07-28
NESTRS_MCP__JSON_RESPONSEfalseAnswer simple operations as JSON rather than SSE
NESTRS_MCP__MAX_REQUEST_BODY_BYTES4194304POST body ceiling, enforced while streaming
NESTRS_MCP__STATELESS_PROTOCOL_METADATA_REQUIREDfalseRequire SEP-2243 per-request protocol metadata

The allowlist is a security control, which is why a deployment reached under a real hostname answers 403 until it names itself. A page on an attacker’s origin can point its own hostname at 127.0.0.1 and POST to a locally running MCP server — which would otherwise answer with the user’s tools and data. Name your hostnames; do not empty the list.

The default looks strict for a public deployment, and that is the point: DNS rebinding only reaches a host the victim’s browser can resolve to but the attacker cannot call directly — loopback and the local network. The vulnerable server is the developer’s local one, which is exactly the one that configures nothing. A default of “off” would disarm the control in the only case the attack works. A refused Host is logged with the offending value; the effective allowlist is logged at debug when the endpoint mounts (target: nest_rs::mcp).

The browser Origin half is not an MCP setting. It is the HTTP transport’s CORS policy, NESTRS_HTTP__CORS_ORIGINS, and /mcp inherits it: the CORS layer wraps the whole route tree, and a disallowed Origin gets 403 on every method, not just the preflight. One knob, and it covers /graphql and /ws too.

McpError (re-exported as nest_rs::mcp::McpError, alias for rmcp’s ErrorData) gives the model a structured failure with a JSON-RPC error code. Six constructors cover the common cases:

ConstructorWhen
McpError::invalid_params(msg, data)The tool input was syntactically valid JSON but semantically wrong — range, missing required field, mutual exclusion
McpError::invalid_request(msg, data)The whole request shape was off
McpError::parse_error(msg, data)The JSON itself couldn’t be parsed
McpError::method_not_found::<MethodName>()The client asked for a tool that doesn’t exist on this server
McpError::resource_not_found(msg, data)An identified resource isn’t there (when you ship resources)
McpError::internal_error(msg, data)Anything else — wrap an upstream failure

A typical adapter for a domain error:

crates/features/src/weather/mcp/tool.rs
fn internal<E: std::fmt::Display>(e: E) -> McpError {
McpError::internal_error(e.to_string(), None)
}

The data field accepts any JSON value — attach structured diagnostics there when the model would benefit from them (e.g. the validator’s per-field errors). Anything beyond these six codes flows through internal_error — there is no custom-code constructor.

  • MCP — the server these options configure.
  • Configuration — the dual-path rule every module follows.