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:
parent
3118fcd6cb
commit
b8c4d951ec
2 changed files with 2854 additions and 28 deletions
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
81
src/ModelContextProtocol.Core/Server/McpHeaderAttribute.cs
Normal file
81
src/ModelContextProtocol.Core/Server/McpHeaderAttribute.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue