Implementation for SEP-2243 Http Standardization (#1553)

Co-authored-by: Tarek Mahmoud Sayed <tarekms@microsoft.com>
Co-authored-by: Tarek Mahmoud Sayed <tarekms@ntdev.microsoft.com>
Co-authored-by: Tarek Mahmoud Sayed <10833894+tarekgh@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Mike Kistler 2026-05-14 17:39:37 -05:00 • committed by GitHub
commit b8c4d951ec
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 2854 additions and 28 deletions

View file

@ -124,6 +124,12 @@ internal sealed partial class AIFunctionMcpServerTool : McpServerTool
Icons = options?.Icons,
};
// Add x-mcp-header extensions to the input schema based on McpHeaderAttribute on parameters
if (function.UnderlyingMethod is { } method)
{
tool.InputSchema = AddMcpHeaderExtensions(tool.InputSchema, method);
}
if (options is not null)
{
if (options.Title is not null ||
@ -600,4 +606,85 @@ internal sealed partial class AIFunctionMcpServerTool : McpServerTool
IsError = allErrorContent && hasAny
};
}
/// <summary>
/// Post-processes the input schema to add <c>x-mcp-header</c> extensions based on
/// <see cref="McpHeaderAttribute"/> on method parameters.
/// </summary>
private static JsonElement AddMcpHeaderExtensions(JsonElement inputSchema, MethodInfo method)
{
// Collect parameters with McpHeaderAttribute
var headerParams = new List<(string ParameterName, string HeaderName, ParameterInfo Parameter)>();
var headerNamesSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (var param in method.GetParameters())
{
var attr = param.GetCustomAttribute<McpHeaderAttribute>();
if (attr is null)
{
continue;
}
// Validate primitive type only
var paramType = Nullable.GetUnderlyingType(param.ParameterType) ?? param.ParameterType;
if (!IsPrimitiveHeaderType(paramType))
{
throw new InvalidOperationException(
$"Parameter '{param.Name}' on method '{method.Name}' has [McpHeader] but is not a primitive type. " +
"Only string, numeric, and boolean types may be annotated with [McpHeader].");
}
// Validate case-insensitive uniqueness
if (!headerNamesSet.Add(attr.Name))
{
throw new InvalidOperationException(
$"Duplicate x-mcp-header name '{attr.Name}' (case-insensitive) found on method '{method.Name}'. " +
"Header names must be case-insensitively unique within a tool's input schema.");
}
headerParams.Add((param.Name!, attr.Name, param));
}
if (headerParams.Count == 0)
{
return inputSchema;
}
// Parse the schema to a mutable JsonNode, add extensions, and convert back
var schemaNode = JsonNode.Parse(inputSchema.GetRawText());
if (schemaNode is not JsonObject schemaObj ||
!schemaObj.TryGetPropertyValue("properties", out var propertiesNode) ||
propertiesNode is not JsonObject propertiesObj)
{
return inputSchema;
}
foreach (var (parameterName, headerName, _) in headerParams)
{
if (propertiesObj.TryGetPropertyValue(parameterName, out var propNode) &&
propNode is JsonObject propObj)
{
propObj["x-mcp-header"] = headerName;
}
}
return JsonSerializer.Deserialize(schemaNode, McpJsonUtilities.JsonContext.Default.JsonElement);
}
private static bool IsPrimitiveHeaderType(Type type)
{
return type == typeof(string) ||
type == typeof(bool) ||
type == typeof(byte) ||
type == typeof(sbyte) ||
type == typeof(short) ||
type == typeof(ushort) ||
type == typeof(int) ||
type == typeof(uint) ||
type == typeof(long) ||
type == typeof(ulong) ||
type == typeof(float) ||
type == typeof(double) ||
type == typeof(decimal);
}
}

View file

@ -0,0 +1,81 @@
namespace ModelContextProtocol.Server;
/// <summary>
/// Indicates that a tool parameter should be mirrored as an HTTP header in client requests.
/// </summary>
/// <remarks>
/// <para>
/// When applied to a parameter, the SDK will include an <c>x-mcp-header</c> extension property
/// in the parameter's JSON schema. Clients will then mirror this parameter's value into an
/// HTTP header named <c>Mcp-Param-{Name}</c>.
/// </para>
/// <para>
/// Only parameters with primitive types (string, number, boolean) may use this attribute.
/// The header name must contain only ASCII characters (0x21-0x7E, excluding space and colon)
/// and must be case-insensitively unique within the tool's input schema.
/// </para>
/// <para>
/// This enables network infrastructure such as load balancers, proxies, and gateways to make
/// routing decisions based on tool parameter values without parsing the JSON-RPC request body.
/// </para>
/// </remarks>
/// <example>
/// <code>
/// [McpServerTool]
/// public static string ExecuteSql(
/// [McpHeader("Region")] string region,
/// string query)
/// {
/// // The client will add header: Mcp-Param-Region: {region value}
/// }
/// </code>
/// </example>
[AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property)]
public sealed class McpHeaderAttribute : Attribute
{
/// <summary>
/// Initializes a new instance of the <see cref="McpHeaderAttribute"/> class.
/// </summary>
/// <param name="name">
/// The name portion of the header. The full header name will be <c>Mcp-Param-{name}</c>.
/// Must contain only ASCII characters (0x21-0x7E, excluding space and colon).
/// </param>
/// <exception cref="ArgumentException">
/// The name is null, empty, or contains invalid characters.
/// </exception>
public McpHeaderAttribute(string name)
{
Throw.IfNullOrWhiteSpace(name);
ValidateHeaderName(name);
Name = name;
}
/// <summary>
/// Gets the name portion of the header.
/// </summary>
/// <remarks>
/// The full header name sent by clients will be <c>Mcp-Param-{Name}</c>.
/// </remarks>
public string Name { get; }
/// <summary>
/// Validates that a header name contains only valid characters.
/// </summary>
/// <param name="name">The header name to validate.</param>
/// <exception cref="ArgumentException">The name contains invalid characters.</exception>
internal static void ValidateHeaderName(string name)
{
foreach (char c in name)
{
// Valid token characters per RFC 9110: visible ASCII (0x21-0x7E) excluding delimiters.
// Space (0x20) and colon (':') are explicitly prohibited.
if (c < 0x21 || c > 0x7E || c == ':')
{
throw new ArgumentException(
$"Header name contains invalid character '{c}' (0x{(int)c:X2}). " +
"Only ASCII characters (0x21-0x7E) excluding colon are allowed.",
nameof(name));
}
}
}
}