Add McpMetaAttribute for attaching metadata to tools, prompts, and resources (#857)

* Initial plan

* Add McpMetaAttribute and integrate with tools, prompts, and resources

Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>

* Add tests and sample usage for McpMetaAttribute

Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>

* Add Meta property to options classes and update logic to merge with attributes

Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>

* Fix missing using directives and clarify McpMetaAttribute documentation

Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>

* Make Name and Value constructor parameters in McpMetaAttribute and check AIFunction.Metadata.UnderlyingMethod

Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>

* Change Value type to object and use JsonSerializer.SerializeToNode, fix nullable warnings

Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>

* Fix UnderlyingMethod property path, use SerializerOptions, and fix JsonObject cref

Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>

* Simplify MethodInfo resolution to use function.UnderlyingMethod directly

Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>

* Fix IL2026 trimming warning by providing type info to JsonSerializer.SerializeToNode

Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>

* Fix remaining issues

* Revert to string-based JSON values for McpMetaAttribute with StringSyntax annotation

Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>

* Add StringSyntax attribute to Value property and fix sample code to use escaped quotes

Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>

* Fix tests and a bit of cleanup

* Change approach and add lots of tests

* Use JsonContext for serialization and fix floating point test comparison

Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>
Co-authored-by: Stephen Toub <stoub@microsoft.com>
This commit is contained in:
Copilot 2025-10-17 09:02:54 -04:00 • committed by GitHub
commit 3bff0adc2f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -6,6 +6,7 @@ using System.Diagnostics;
using System.Reflection;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.Json.Serialization.Metadata;
using System.Text.RegularExpressions;
namespace ModelContextProtocol.Server;
@ -143,6 +144,11 @@ internal sealed partial class AIFunctionMcpServerTool : McpServerTool
ReadOnlyHint = options.ReadOnly,
};
}
// Populate Meta from options and/or McpMetaAttribute instances if a MethodInfo is available
tool.Meta = function.UnderlyingMethod is not null ?
CreateMetaFromAttributes(function.UnderlyingMethod, options.Meta, options.SerializerOptions) :
options.Meta;
}
return new AIFunctionMcpServerTool(function, tool, options?.Services, structuredOutputRequiresWrapping, options?.Metadata ?? []);
@ -351,6 +357,25 @@ internal sealed partial class AIFunctionMcpServerTool : McpServerTool
return metadata.AsReadOnly();
}
/// <summary>Creates a Meta <see cref="JsonObject"/> from <see cref="McpMetaAttribute"/> instances on the specified method.</summary>
/// <param name="method">The method to extract <see cref="McpMetaAttribute"/> instances from.</param>
/// <param name="meta">Optional <see cref="JsonObject"/> to seed the Meta with. Properties from this object take precedence over attributes.</param>
/// <param name="serializerOptions">Optional <see cref="JsonSerializerOptions"/> to use for serialization. This parameter is ignored when parsing JSON strings from attributes.</param>
/// <returns>A <see cref="JsonObject"/> with metadata, or null if no metadata is present.</returns>
internal static JsonObject? CreateMetaFromAttributes(MethodInfo method, JsonObject? meta = null, JsonSerializerOptions? serializerOptions = null)
{
// Transfer all McpMetaAttribute instances to the Meta JsonObject, ignoring any that would overwrite existing properties.
foreach (var attr in method.GetCustomAttributes<McpMetaAttribute>())
{
if (meta?.ContainsKey(attr.Name) is not true)
{
(meta ??= [])[attr.Name] = JsonNode.Parse(attr.JsonValue);
}
}
return meta;
}
/// <summary>Regex that flags runs of characters other than ASCII digits or letters.</summary>
#if NET
[GeneratedRegex("[^0-9A-Za-z]+")]