ConduitMcp.Headers (ConduitMCP v0.11.0)

Copy Markdown View Source

HTTP request-metadata header codec for the MCP 2026-07-28 Streamable HTTP transport.

The transport mirrors selected JSON-RPC body fields into HTTP headers so intermediaries can route requests without parsing the body:

  • Mcp-Method — the request method, on every request.
  • Mcp-Name — params.name for tools/call and prompts/get, params.uri for resources/read, and params.taskId for the tasks extension's tasks/get, tasks/update and tasks/cancel (ext-tasks routing headers).
  • Mcp-Param-{Name} — tool arguments whose inputSchema property carries an "x-mcp-header": "{Name}" annotation.

This module is pure: it builds those headers for a client, validates x-mcp-header annotations, and lets a server decode header values and compare them to body values. Header names it returns are lowercase, matching how Plug normalizes request header names.

Value encoding

Strings are sent as-is, integers as decimal, booleans as true/false. A string that is not plain header-safe ASCII — it contains a byte outside 0x20..0x7E (non-ASCII, control characters including tab and DEL), has a leading or trailing space, or starts with =? and ends with ?= (so it could be mistaken for the sentinel) — is sent as =?base64?{standard padded Base64 of its UTF-8 bytes}?=.

Integer comparison is numeric and strict: a header matches an integer body value only when it is a canonical decimal integer (optional -, no leading zeros other than "0" itself, no "-0") within the JavaScript safe range -(2^53 - 1)..(2^53 - 1). Body values 42 and 42.0 both match "42".

Summary

Types

An x-mcp-header annotation reachable from the schema root.

Functions

Decodes a received header value.

Encodes a string, integer, or boolean as a header value.

Lists the x-mcp-header annotations reachable from the root of an inputSchema through chains of properties keys only.

Returns the Mcp-Method header for a request method.

Returns the Mcp-Name header for a request, or nil when none applies.

Returns whether requests with method must carry an Mcp-Name header.

The params key Mcp-Name mirrors for method ("name", "uri" or "taskId"), or nil when the method carries no Mcp-Name.

Builds the Mcp-Param-{Name} headers for a tool call.

Validates every x-mcp-header annotation in an inputSchema.

Compares a decoded header value to the corresponding body value.

Types

header_param()

@type header_param() :: %{header: String.t(), path: [String.t()], type: String.t()}

An x-mcp-header annotation reachable from the schema root.

Functions

decode_value(value)

@spec decode_value(String.t()) :: {:ok, String.t()} | :error

Decodes a received header value.

A value that starts with =?base64? and ends with ?= must carry valid, padded standard Base64 of valid UTF-8 and decodes to that string; anything else in that shape returns :error. Every other value is returned unchanged.

encode_value(value)

@spec encode_value(String.t() | integer() | boolean()) :: String.t()

Encodes a string, integer, or boolean as a header value.

Strings that are not plain header-safe ASCII are wrapped in the =?base64?...?= sentinel; see the module documentation.

header_params(schema)

@spec header_params(map()) :: [header_param()]

Lists the x-mcp-header annotations reachable from the root of an inputSchema through chains of properties keys only.

Each entry has the declared header Name as written, the property-key path from the root, and the property's declared type. Annotations whose Name or type is not a string are left out; validate_x_mcp_header/1 reports those. Entries are ordered by path.

method_header(method)

@spec method_header(String.t()) :: {String.t(), String.t()}

Returns the Mcp-Method header for a request method.

name_header(method, params)

@spec name_header(String.t(), map()) :: {String.t(), String.t()} | nil

Returns the Mcp-Name header for a request, or nil when none applies.

Only tools/call and prompts/get (from params["name"]), resources/read (from params["uri"]) and the tasks extension's tasks/get|update|cancel (from params["taskId"]) carry the header, and only when that param is a string. The value is encoded with encode_value/1.

name_header_required?(method)

@spec name_header_required?(String.t()) :: boolean()

Returns whether requests with method must carry an Mcp-Name header.

name_param(method)

@spec name_param(String.t()) :: String.t() | nil

The params key Mcp-Name mirrors for method ("name", "uri" or "taskId"), or nil when the method carries no Mcp-Name.

param_headers(tool_or_schema, args)

@spec param_headers(map(), map()) :: [{String.t(), String.t()}]

Builds the Mcp-Param-{Name} headers for a tool call.

tool_or_schema is a tool definition (with "inputSchema") or an inputSchema itself; args are the string-keyed call arguments. A header is produced for each annotation from header_params/1 whose argument value at path is a string, integer, boolean, or integral float (42.0 encodes as "42"). Absent and nil values, and values of any other type, produce no header. Header names are "mcp-param-" followed by the lowercased Name.

validate_x_mcp_header(schema)

@spec validate_x_mcp_header(map()) :: :ok | {:error, [String.t()]}

Validates every x-mcp-header annotation in an inputSchema.

The whole schema is searched, including items, composition keywords and $defs. Each annotation must:

  • sit on a property reachable from the root through properties keys only;
  • be on a property whose "type" is "string", "integer" or "boolean";
  • be a non-empty RFC 9110 token (which excludes CR and LF);
  • be unique, compared case-insensitively, within the schema.

Errors name the annotation and its JSON Pointer location, never argument values.

value_matches?(header, body)

@spec value_matches?(String.t(), term()) :: boolean()

Compares a decoded header value to the corresponding body value.

Strings compare exactly and booleans against "true"/"false". Integers and floats compare numerically against a canonical decimal integer header within the JavaScript safe range ("42" matches 42 and 42.0; "042", "-0", "+1" and "42.0" match nothing). Any other body value, including nil, never matches.