Skip to content

Commit 3bff0ad

Browse files
Copilotstephentoub
andauthored
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 <[email protected]> * Add tests and sample usage for McpMetaAttribute Co-authored-by: stephentoub <[email protected]> * Add Meta property to options classes and update logic to merge with attributes Co-authored-by: stephentoub <[email protected]> * Fix missing using directives and clarify McpMetaAttribute documentation Co-authored-by: stephentoub <[email protected]> * Make Name and Value constructor parameters in McpMetaAttribute and check AIFunction.Metadata.UnderlyingMethod Co-authored-by: stephentoub <[email protected]> * Change Value type to object and use JsonSerializer.SerializeToNode, fix nullable warnings Co-authored-by: stephentoub <[email protected]> * Fix UnderlyingMethod property path, use SerializerOptions, and fix JsonObject cref Co-authored-by: stephentoub <[email protected]> * Simplify MethodInfo resolution to use function.UnderlyingMethod directly Co-authored-by: stephentoub <[email protected]> * Fix IL2026 trimming warning by providing type info to JsonSerializer.SerializeToNode Co-authored-by: stephentoub <[email protected]> * Fix remaining issues * Revert to string-based JSON values for McpMetaAttribute with StringSyntax annotation Co-authored-by: stephentoub <[email protected]> * Add StringSyntax attribute to Value property and fix sample code to use escaped quotes Co-authored-by: stephentoub <[email protected]> * 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 <[email protected]> --------- Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: stephentoub <[email protected]> Co-authored-by: Stephen Toub <[email protected]>
1 parent fb07d66 commit 3bff0ad

File tree

12 files changed

+1734
-3
lines changed

12 files changed

+1734
-3
lines changed

samples/AspNetCoreMcpServer/Tools/WeatherTools.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ public WeatherTools(IHttpClientFactory httpClientFactory)
1717
}
1818

1919
[McpServerTool, Description("Get weather alerts for a US state.")]
20+
[McpMeta("category", "weather")]
21+
[McpMeta("dataSource", "weather.gov")]
2022
public async Task<string> GetAlerts(
2123
[Description("The US state to get alerts for. Use the 2 letter abbreviation for the state (e.g. NY).")] string state)
2224
{
@@ -46,6 +48,8 @@ public async Task<string> GetAlerts(
4648
}
4749

4850
[McpServerTool, Description("Get weather forecast for a location.")]
51+
[McpMeta("category", "weather")]
52+
[McpMeta("recommendedModel", "gpt-4")]
4953
public async Task<string> GetForecast(
5054
[Description("Latitude of the location.")] double latitude,
5155
[Description("Longitude of the location.")] double longitude)

src/ModelContextProtocol.Core/Protocol/ClientCapabilities.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ public sealed class ClientCapabilities
4747
/// </para>
4848
/// <para>
4949
/// The server can use <see cref="McpServer.RequestRootsAsync"/> to request the list of
50-
/// available roots from the client, which will trigger the client's <see cref="ModelContextProtocol.Client.McpClientHandlers.RootsHandler"/>.
50+
/// available roots from the client, which will trigger the client's <see cref="McpClientHandlers.RootsHandler"/>.
5151
/// </para>
5252
/// </remarks>
5353
[JsonPropertyName("roots")]

src/ModelContextProtocol.Core/Protocol/ElicitationCapability.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ namespace ModelContextProtocol.Protocol;
1313
/// </para>
1414
/// <para>
1515
/// When this capability is enabled, an MCP server can request the client to provide additional information
16-
/// during interactions. The client must set a <see cref="ModelContextProtocol.Client.McpClientHandlers.ElicitationHandler"/> to process these requests.
16+
/// during interactions. The client must set a <see cref="McpClientHandlers.ElicitationHandler"/> to process these requests.
1717
/// </para>
1818
/// <para>
1919
/// This class is intentionally empty as the Model Context Protocol specification does not

src/ModelContextProtocol.Core/Protocol/SamplingCapability.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ namespace ModelContextProtocol.Protocol;
1414
/// </para>
1515
/// <para>
1616
/// When this capability is enabled, an MCP server can request the client to generate content
17-
/// using an AI model. The client must set a <see cref="ModelContextProtocol.Client.McpClientHandlers.SamplingHandler"/> to process these requests.
17+
/// using an AI model. The client must set a <see cref="McpClientHandlers.SamplingHandler"/> to process these requests.
1818
/// </para>
1919
/// <para>
2020
/// This class is intentionally empty as the Model Context Protocol specification does not

src/ModelContextProtocol.Core/Server/AIFunctionMcpServerPrompt.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.Diagnostics;
66
using System.Reflection;
77
using System.Text.Json;
8+
using System.Text.Json.Nodes;
89

910
namespace ModelContextProtocol.Server;
1011

@@ -138,6 +139,11 @@ private static AIFunctionFactoryOptions CreateAIFunctionFactoryOptions(
138139
Icons = options?.Icons,
139140
};
140141

142+
// Populate Meta from options and/or McpMetaAttribute instances if a MethodInfo is available
143+
prompt.Meta = function.UnderlyingMethod is not null ?
144+
AIFunctionMcpServerTool.CreateMetaFromAttributes(function.UnderlyingMethod, options?.Meta, options?.SerializerOptions) :
145+
options?.Meta;
146+
141147
return new AIFunctionMcpServerPrompt(function, prompt, options?.Metadata ?? []);
142148
}
143149

src/ModelContextProtocol.Core/Server/AIFunctionMcpServerResource.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
using System.Runtime.CompilerServices;
1010
using System.Text;
1111
using System.Text.Json;
12+
using System.Text.Json.Nodes;
1213
using System.Text.RegularExpressions;
1314

1415
namespace ModelContextProtocol.Server;
@@ -219,6 +220,9 @@ private static AIFunctionFactoryOptions CreateAIFunctionFactoryOptions(
219220
Description = options?.Description,
220221
MimeType = options?.MimeType ?? "application/octet-stream",
221222
Icons = options?.Icons,
223+
Meta = function.UnderlyingMethod is not null ?
224+
AIFunctionMcpServerTool.CreateMetaFromAttributes(function.UnderlyingMethod, options?.Meta, options?.SerializerOptions) :
225+
options?.Meta,
222226
};
223227

224228
return new AIFunctionMcpServerResource(function, resource, options?.Metadata ?? []);

src/ModelContextProtocol.Core/Server/AIFunctionMcpServerTool.cs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using System.Reflection;
77
using System.Text.Json;
88
using System.Text.Json.Nodes;
9+
using System.Text.Json.Serialization.Metadata;
910
using System.Text.RegularExpressions;
1011

1112
namespace ModelContextProtocol.Server;
@@ -143,6 +144,11 @@ options.OpenWorld is not null ||
143144
ReadOnlyHint = options.ReadOnly,
144145
};
145146
}
147+
148+
// Populate Meta from options and/or McpMetaAttribute instances if a MethodInfo is available
149+
tool.Meta = function.UnderlyingMethod is not null ?
150+
CreateMetaFromAttributes(function.UnderlyingMethod, options.Meta, options.SerializerOptions) :
151+
options.Meta;
146152
}
147153

148154
return new AIFunctionMcpServerTool(function, tool, options?.Services, structuredOutputRequiresWrapping, options?.Metadata ?? []);
@@ -351,6 +357,25 @@ internal static IReadOnlyList<object> CreateMetadata(MethodInfo method)
351357
return metadata.AsReadOnly();
352358
}
353359

360+
/// <summary>Creates a Meta <see cref="JsonObject"/> from <see cref="McpMetaAttribute"/> instances on the specified method.</summary>
361+
/// <param name="method">The method to extract <see cref="McpMetaAttribute"/> instances from.</param>
362+
/// <param name="meta">Optional <see cref="JsonObject"/> to seed the Meta with. Properties from this object take precedence over attributes.</param>
363+
/// <param name="serializerOptions">Optional <see cref="JsonSerializerOptions"/> to use for serialization. This parameter is ignored when parsing JSON strings from attributes.</param>
364+
/// <returns>A <see cref="JsonObject"/> with metadata, or null if no metadata is present.</returns>
365+
internal static JsonObject? CreateMetaFromAttributes(MethodInfo method, JsonObject? meta = null, JsonSerializerOptions? serializerOptions = null)
366+
{
367+
// Transfer all McpMetaAttribute instances to the Meta JsonObject, ignoring any that would overwrite existing properties.
368+
foreach (var attr in method.GetCustomAttributes<McpMetaAttribute>())
369+
{
370+
if (meta?.ContainsKey(attr.Name) is not true)
371+
{
372+
(meta ??= [])[attr.Name] = JsonNode.Parse(attr.JsonValue);
373+
}
374+
}
375+
376+
return meta;
377+
}
378+
354379
/// <summary>Regex that flags runs of characters other than ASCII digits or letters.</summary>
355380
#if NET
356381
[GeneratedRegex("[^0-9A-Za-z]+")]
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
using ModelContextProtocol.Protocol;
2+
using System.Diagnostics.CodeAnalysis;
3+
using System.Text.Json;
4+
using System.Text.Json.Nodes;
5+
6+
namespace ModelContextProtocol.Server;
7+
8+
/// <summary>
9+
/// Used to specify metadata for an MCP server primitive (tool, prompt, or resource).
10+
/// </summary>
11+
/// <remarks>
12+
/// <para>
13+
/// The metadata is used to populate the <see cref="Tool.Meta"/>, <see cref="Prompt.Meta"/>,
14+
/// or <see cref="Resource.Meta"/> property of the corresponding primitive.
15+
/// </para>
16+
/// <para>
17+
/// This attribute can be applied multiple times to a method to specify multiple key/value pairs
18+
/// of metadata. However, the same key should not be used more than once; doing so will result
19+
/// in undefined behavior.
20+
/// </para>
21+
/// <para>
22+
/// Metadata can be used to attach additional information to primitives, such as model preferences,
23+
/// version information, or other custom data that should be communicated to MCP clients.
24+
/// </para>
25+
/// </remarks>
26+
/// <example>
27+
/// <code>
28+
/// [McpServerTool]
29+
/// [McpMeta("model", "gpt-4o")]
30+
/// [McpMeta("version", "1.0")]
31+
/// [McpMeta("priority", 5.0)]
32+
/// [McpMeta("isBeta", true)]
33+
/// [McpMeta("tags", JsonValue = """["a","b"]""")]
34+
/// public string MyTool(string input) => $"Processed: {input}";
35+
/// </code>
36+
/// </example>
37+
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
38+
public sealed class McpMetaAttribute : Attribute
39+
{
40+
/// <summary>
41+
/// Initializes a new instance of the <see cref="McpMetaAttribute"/> class with a string value.
42+
/// </summary>
43+
/// <param name="name">The name (key) of the metadata entry.</param>
44+
/// <param name="value">The string value of the metadata entry. If null, the value will be serialized as JSON null.</param>
45+
public McpMetaAttribute(string name, string? value = null)
46+
{
47+
Name = name;
48+
JsonValue = value is null ? "null" : JsonSerializer.Serialize(value, McpJsonUtilities.JsonContext.Default.String);
49+
}
50+
51+
/// <summary>
52+
/// Initializes a new instance of the <see cref="McpMetaAttribute"/> class with a double value.
53+
/// </summary>
54+
/// <param name="name">The name (key) of the metadata entry.</param>
55+
/// <param name="value">The double value of the metadata entry.</param>
56+
public McpMetaAttribute(string name, double value)
57+
{
58+
Name = name;
59+
JsonValue = JsonSerializer.Serialize(value, McpJsonUtilities.DefaultOptions.GetTypeInfo(typeof(double)));
60+
}
61+
62+
/// <summary>
63+
/// Initializes a new instance of the <see cref="McpMetaAttribute"/> class with a boolean value.
64+
/// </summary>
65+
/// <param name="name">The name (key) of the metadata entry.</param>
66+
/// <param name="value">The boolean value of the metadata entry.</param>
67+
public McpMetaAttribute(string name, bool value)
68+
{
69+
Name = name;
70+
JsonValue = JsonSerializer.Serialize(value, McpJsonUtilities.JsonContext.Default.Boolean);
71+
}
72+
73+
/// <summary>
74+
/// Gets the name (key) of the metadata entry.
75+
/// </summary>
76+
/// <remarks>
77+
/// This value is used as the key in the metadata object. It should be a unique identifier
78+
/// for this piece of metadata within the context of the primitive.
79+
/// </remarks>
80+
public string Name { get; }
81+
82+
/// <summary>
83+
/// Gets or sets the value of the metadata entry as a JSON string.
84+
/// </summary>
85+
/// <remarks>
86+
/// <para>
87+
/// This value must be well-formed JSON. It will be parsed and added to the metadata <see cref="JsonObject"/>.
88+
/// Simple values can be represented as JSON literals like <c>"\"my-string\""</c>, <c>"123"</c>,
89+
/// <c>"true"</c>, etc. Complex structures can be represented as JSON objects or arrays.
90+
/// </para>
91+
/// <para>
92+
/// Setting this property will override any value provided via the constructor.
93+
/// </para>
94+
/// <para>
95+
/// For programmatic scenarios where you want to construct complex metadata without dealing with
96+
/// JSON strings, use the <see cref="McpServerToolCreateOptions.Meta"/>,
97+
/// <see cref="McpServerPromptCreateOptions.Meta"/>, or <see cref="McpServerResourceCreateOptions.Meta"/>
98+
/// property to provide a JsonObject directly.
99+
/// </para>
100+
/// </remarks>
101+
[StringSyntax(StringSyntaxAttribute.Json)]
102+
public string JsonValue { get; set; }
103+
}

src/ModelContextProtocol.Core/Server/McpServerPromptCreateOptions.cs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using ModelContextProtocol.Protocol;
33
using System.ComponentModel;
44
using System.Text.Json;
5+
using System.Text.Json.Nodes;
56

67
namespace ModelContextProtocol.Server;
78

@@ -86,6 +87,21 @@ public sealed class McpServerPromptCreateOptions
8687
/// </remarks>
8788
public IList<Icon>? Icons { get; set; }
8889

90+
/// <summary>
91+
/// Gets or sets metadata reserved by MCP for protocol-level metadata.
92+
/// </summary>
93+
/// <remarks>
94+
/// <para>
95+
/// This <see cref="JsonObject"/> is used to seed the <see cref="Prompt.Meta"/> property. Any metadata from
96+
/// <see cref="McpMetaAttribute"/> instances on the method will be added to this object, but
97+
/// properties already present in this <see cref="JsonObject"/> will not be overwritten.
98+
/// </para>
99+
/// <para>
100+
/// Implementations must not make assumptions about its contents.
101+
/// </para>
102+
/// </remarks>
103+
public JsonObject? Meta { get; set; }
104+
89105
/// <summary>
90106
/// Creates a shallow clone of the current <see cref="McpServerPromptCreateOptions"/> instance.
91107
/// </summary>
@@ -100,5 +116,6 @@ internal McpServerPromptCreateOptions Clone() =>
100116
SchemaCreateOptions = SchemaCreateOptions,
101117
Metadata = Metadata,
102118
Icons = Icons,
119+
Meta = Meta,
103120
};
104121
}

src/ModelContextProtocol.Core/Server/McpServerResourceCreateOptions.cs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using ModelContextProtocol.Protocol;
33
using System.ComponentModel;
44
using System.Text.Json;
5+
using System.Text.Json.Nodes;
56

67
namespace ModelContextProtocol.Server;
78

@@ -101,6 +102,21 @@ public sealed class McpServerResourceCreateOptions
101102
/// </remarks>
102103
public IList<Icon>? Icons { get; set; }
103104

105+
/// <summary>
106+
/// Gets or sets metadata reserved by MCP for protocol-level metadata.
107+
/// </summary>
108+
/// <remarks>
109+
/// <para>
110+
/// This <see cref="JsonObject"/> is used to seed the <see cref="Resource.Meta"/> property. Any metadata from
111+
/// <see cref="McpMetaAttribute"/> instances on the method will be added to this object, but
112+
/// properties already present in this <see cref="JsonObject"/> will not be overwritten.
113+
/// </para>
114+
/// <para>
115+
/// Implementations must not make assumptions about its contents.
116+
/// </para>
117+
/// </remarks>
118+
public JsonObject? Meta { get; set; }
119+
104120
/// <summary>
105121
/// Creates a shallow clone of the current <see cref="McpServerResourceCreateOptions"/> instance.
106122
/// </summary>
@@ -117,5 +133,6 @@ internal McpServerResourceCreateOptions Clone() =>
117133
SchemaCreateOptions = SchemaCreateOptions,
118134
Metadata = Metadata,
119135
Icons = Icons,
136+
Meta = Meta,
120137
};
121138
}

0 commit comments

Comments
 (0)