Module: MCP::Client::McpParamHeaders

Defined in:
lib/mcp/client/mcp_param_headers.rb

Overview

The custom-header half of SEP-2243 (MCP 2026-07-28): scanning a tool's inputSchema for x-mcp-header declarations and encoding tools/call argument values into Mcp-Param-{Name} HTTP headers, with the =?base64?...?= sentinel for values that cannot ride as plain ASCII field values. Mirrors the TypeScript SDK's mcpParamHeaders codec; the standard-header half (Mcp-Method, Mcp-Name) lives with the transport.

https://modelcontextprotocol.io/specification/draft/basic/transports/streamable-http#custom-headers-from-tool-parameters

Constant Summary collapse

HEADER_PREFIX =

The fixed prefix every custom-parameter header carries.

"Mcp-Param-"
X_MCP_HEADER_KEY =

The schema-extension property name a tool's inputSchema carries.

"x-mcp-header"
RFC9110_TOKEN =

RFC 9110 Section 5.1 token syntax (1*tchar): rejects empty names, spaces, control characters (including CR/LF), and the HTTP delimiters.

/\A[!#$%&'*+\-.^_`|~0-9A-Za-z]+\z/.freeze
PERMITTED_TYPES =

The spec text admits string, integer, and boolean. number is also accepted because the published conformance referee annotates type: "number" parameters and expects them mirrored; the TypeScript SDK makes the same accommodation.

["string", "integer", "boolean", "number"].freeze
NON_REACHABLE_SUBSCHEMA_KEYWORDS =

JSON Schema keywords the SEP-2243 static-reachability constraint excludes from the properties-only chain. An x-mcp-header under any of these invalidates the tool definition rather than being silently ignored.

[
  "items",
  "prefixItems",
  "contains",
  "additionalProperties",
  "unevaluatedProperties",
  "unevaluatedItems",
  "propertyNames",
  "patternProperties",
  "dependentSchemas",
  "oneOf",
  "anyOf",
  "allOf",
  "not",
  "if",
  "then",
  "else",
  "$defs",
  "definitions",
].freeze
OBJECT_VALUED_SUBSCHEMA_KEYWORDS =

Keywords whose value maps names to subschemas rather than being one subschema or a list of them.

["patternProperties", "dependentSchemas", "$defs", "definitions"].freeze
MAX_SAFE_INTEGER =

Integers beyond 2**53 - 1 lose precision in JSON number interchange, so they are not mirrored; the TypeScript SDK refuses unsafe integers the same way.

(2**53) - 1
BASE64_SENTINEL_PREFIX =
"=?base64?"
BASE64_SENTINEL_SUFFIX =
"?="

Class Method Summary collapse

Class Method Details

.build(declarations, arguments) ⇒ Object

Builds the Mcp-Param-{Name} headers for one tools/call from the scanned declarations and the call's arguments. A null or absent value omits its header (the spec's MUST-omit rows); a non-primitive or non-representable value is omitted rather than emitted malformed.



78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
# File 'lib/mcp/client/mcp_param_headers.rb', line 78

def build(declarations, arguments)
  declarations.each_with_object({}) do |declaration, headers|
    value = value_at_path(arguments, declaration[:path])
    next if value.nil?

    string_value = primitive_to_string(value)
    next unless string_value

    encoded = begin
      encode_value(string_value)
    rescue EncodingError
      # A string that cannot be represented as UTF-8 (e.g. binary data) has no header
      # representation; omit it like the other non-representable values.
      next
    end

    headers["#{HEADER_PREFIX}#{declaration[:header_name]}"] = encoded
  end
end

.encode_value(value) ⇒ Object

Encodes a header value per the spec's value-encoding rules: a safe plain-ASCII field value passes through unchanged, everything else is wrapped as =?base64?{base64-of-UTF-8}?=.



122
123
124
125
126
# File 'lib/mcp/client/mcp_param_headers.rb', line 122

def encode_value(value)
  return value unless needs_base64?(value)

  "#{BASE64_SENTINEL_PREFIX}#{[value.encode(Encoding::UTF_8)].pack("m0")}#{BASE64_SENTINEL_SUFFIX}"
end

.primitive_to_string(value) ⇒ Object

Converts a primitive argument to its header string per the spec's type-conversion rules: strings pass through, booleans become lowercase "true" / "false", and numbers become their decimal string. nil means "not representable: do not emit a header".



101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
# File 'lib/mcp/client/mcp_param_headers.rb', line 101

def primitive_to_string(value)
  case value
  when String
    value
  when true
    "true"
  when false
    "false"
  when Integer
    value.abs <= MAX_SAFE_INTEGER ? value.to_s : nil
  when Float
    return unless value.finite?

    # JSON has one number type: an integral float serializes without the fractional part,
    # matching the `String(42.0)` the JavaScript reference emits.
    value == value.truncate ? value.truncate.to_s : value.to_s
  end
end

.scan(input_schema) ⇒ Object

Scans a tool's inputSchema for x-mcp-header declarations and validates every constraint the spec places on them: RFC 9110 token names, case-insensitive uniqueness, primitive-typed declaring properties, and static reachability through a chain of properties keys only. Returns { valid: true, declarations: [...] } with each declaration { path:, header_name:, type: }, or { valid: false, reason: "..." } on the first violation.



68
69
70
71
72
73
# File 'lib/mcp/client/mcp_param_headers.rb', line 68

def scan(input_schema)
  declarations = []
  fault = visit(input_schema, [], true, declarations, {})

  fault ? { valid: false, reason: fault } : { valid: true, declarations: declarations }
end