systemprompt-mcp 0.41.0

Native Model Context Protocol (MCP) implementation for systemprompt.io. Orchestration, per-server OAuth2, RBAC middleware, and tool-call governance — the core of the AI governance pipeline.
Documentation
//! `_meta` stamping for outbound MCP requests.
//!
//! Copyright (c) systemprompt.io — Business Source License 1.1.
//! See <https://systemprompt.io> for licensing details.

use http::{HeaderName, HeaderValue};
use rmcp::model::{ClientCapabilities, ClientJsonRpcMessage, GetMeta, ProtocolVersion};
use std::collections::HashMap;

// Why: SEP-2575. From 2026-07-28 a server rejects any non-initialize request
// whose `_meta` omits the negotiated protocol version and the client's
// capabilities — "request _meta is missing or has malformed required fields" —
// because a stateless server has no session to remember them from. rmcp's
// client sets the `MCP-Protocol-Version` HEADER but never the matching `_meta`
// fields, so without this every call fails at the transport, before any
// handler runs. Below 2026-07-28 nothing is stamped and nothing changes.
//
// Stamping here also settles the SEP-2243 headers: rmcp derives `Mcp-Method`
// and `Mcp-Name` from the negotiated version, which is 2026-07-28 exactly when
// this applies, so it adds them itself.
pub(super) fn stamp_request_metadata(
    message: &mut ClientJsonRpcMessage,
    custom_headers: &HashMap<HeaderName, HeaderValue>,
    client_capabilities: &ClientCapabilities,
) {
    let ClientJsonRpcMessage::Request(request) = message else {
        return;
    };
    // Why: The negotiated version, as rmcp resolved it at `initialize` and now
    // echoes on every request. Reading it back rather than assuming a version
    // is what keeps an older or third-party server unaffected, and it
    // guarantees the header and the `_meta` field agree — the server rejects a
    // mismatch as loudly as it rejects an omission.
    let Some(negotiated) = custom_headers
        .get(&HeaderName::from_static(HEADER_MCP_PROTOCOL_VERSION_LOWER))
        .and_then(|value| value.to_str().ok())
    else {
        return;
    };
    if negotiated < ProtocolVersion::V_2026_07_28.as_str() {
        return;
    }
    // Why: resolved against the SDK's own list rather than reconstructed from
    // the string. A version this client does not know is one whose `_meta`
    // contract it cannot claim to satisfy, so it is left alone.
    let Some(version) = ProtocolVersion::KNOWN_VERSIONS
        .iter()
        .find(|known| known.as_str() == negotiated)
    else {
        return;
    };

    let meta = request.request.get_meta_mut();
    if meta.protocol_version().is_none() {
        meta.set_protocol_version(version.clone());
    }
    if meta.client_capabilities().is_none() {
        meta.set_client_capabilities(client_capabilities.clone());
    }
}

// Why: `HeaderName::from_static` panics on an uppercase byte, and rmcp's
// constant is the canonical mixed-case spelling.
const HEADER_MCP_PROTOCOL_VERSION_LOWER: &str = "mcp-protocol-version";