Skip to content

Commit 6cea3bc

Browse files
Copilotstephentoub
andcommitted
Revert to string-based JSON values for McpMetaAttribute with StringSyntax annotation
Co-authored-by: stephentoub <[email protected]>
1 parent 690b18d commit 6cea3bc

File tree

4 files changed

+38
-56
lines changed

4 files changed

+38
-56
lines changed

samples/AspNetCoreMcpServer/Tools/WeatherTools.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +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")]
20+
[McpMeta("category", "\"weather\"")]
21+
[McpMeta("dataSource", "\"weather.gov\"")]
2222
public async Task<string> GetAlerts(
2323
[Description("The US state to get alerts for. Use the 2 letter abbreviation for the state (e.g. NY).")] string state)
2424
{

src/ModelContextProtocol.Core/Server/AIFunctionMcpServerTool.cs

Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -360,7 +360,7 @@ internal static IReadOnlyList<object> CreateMetadata(MethodInfo method)
360360
/// <summary>Creates a Meta <see cref="JsonObject"/> from <see cref="McpMetaAttribute"/> instances on the specified method.</summary>
361361
/// <param name="method">The method to extract <see cref="McpMetaAttribute"/> instances from.</param>
362362
/// <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. Defaults to <see cref="McpJsonUtilities.DefaultOptions"/> if not provided.</param>
363+
/// <param name="serializerOptions">Optional <see cref="JsonSerializerOptions"/> to use for serialization. This parameter is ignored when parsing JSON strings from attributes.</param>
364364
/// <returns>A <see cref="JsonObject"/> with metadata, or null if no metadata is present.</returns>
365365
internal static JsonObject? CreateMetaFromAttributes(MethodInfo method, JsonObject? meta = null, JsonSerializerOptions? serializerOptions = null)
366366
{
@@ -372,20 +372,8 @@ internal static IReadOnlyList<object> CreateMetadata(MethodInfo method)
372372
meta ??= [];
373373
if (!meta.ContainsKey(attr.Name))
374374
{
375-
JsonTypeInfo? valueTypeInfo = null;
376-
if (attr.Value?.GetType() is { } valueType)
377-
{
378-
if (serializerOptions?.TryGetTypeInfo(valueType, out valueTypeInfo) is not true &&
379-
McpJsonUtilities.DefaultOptions.TryGetTypeInfo(valueType, out valueTypeInfo) is not true)
380-
{
381-
// Throw using GetTypeInfo in order to get a good exception message.
382-
(serializerOptions ?? McpJsonUtilities.DefaultOptions).GetTypeInfo(valueType);
383-
}
384-
385-
Debug.Assert(valueTypeInfo is not null, "GetTypeInfo should have thrown an exception");
386-
}
387-
388-
meta[attr.Name] = valueTypeInfo is not null ? JsonSerializer.SerializeToNode(attr.Value, valueTypeInfo) : null;
375+
// Parse the JSON string value into a JsonNode
376+
meta[attr.Name] = JsonNode.Parse(attr.Value);
389377
}
390378
}
391379

src/ModelContextProtocol.Core/Server/McpMetaAttribute.cs

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using ModelContextProtocol.Protocol;
2+
using System.Diagnostics.CodeAnalysis;
23

34
namespace ModelContextProtocol.Server;
45

@@ -18,8 +19,9 @@ namespace ModelContextProtocol.Server;
1819
/// <example>
1920
/// <code>
2021
/// [McpServerTool]
21-
/// [McpMeta("model", "gpt-4o")]
22-
/// [McpMeta("version", "1.0")]
22+
/// [McpMeta("model", "\"gpt-4o\"")]
23+
/// [McpMeta("version", "\"1.0\"")]
24+
/// [McpMeta("priority", "5")]
2325
/// public string MyTool(string input)
2426
/// {
2527
/// return $"Processed: {input}";
@@ -34,8 +36,8 @@ public sealed class McpMetaAttribute : Attribute
3436
/// Initializes a new instance of the <see cref="McpMetaAttribute"/> class.
3537
/// </summary>
3638
/// <param name="name">The name (key) of the metadata entry.</param>
37-
/// <param name="value">The value of the metadata entry. This can be any value that can be encoded in .NET metadata.</param>
38-
public McpMetaAttribute(string name, object? value)
39+
/// <param name="value">The value of the metadata entry as a JSON string. This must be well-formed JSON.</param>
40+
public McpMetaAttribute(string name, [StringSyntax(StringSyntaxAttribute.Json)] string value)
3941
{
4042
Name = name;
4143
Value = value;
@@ -51,19 +53,20 @@ public McpMetaAttribute(string name, object? value)
5153
public string Name { get; }
5254

5355
/// <summary>
54-
/// Gets the value of the metadata entry.
56+
/// Gets the value of the metadata entry as a JSON string.
5557
/// </summary>
5658
/// <remarks>
5759
/// <para>
58-
/// This value can be any object that can be encoded in .NET metadata (strings, numbers, booleans, etc.).
59-
/// The value will be serialized to JSON using <see cref="System.Text.Json.JsonSerializer"/> when
60-
/// populating the metadata JsonObject.
60+
/// This value must be well-formed JSON. It will be parsed and added to the metadata JsonObject.
61+
/// Simple values can be represented as JSON literals like <c>"\"my-string\""</c>, <c>"123"</c>,
62+
/// <c>"true"</c>, etc. Complex structures can be represented as JSON objects or arrays.
6163
/// </para>
6264
/// <para>
63-
/// For complex JSON structures that cannot be represented as .NET metadata, use the
64-
/// <see cref="McpServerToolCreateOptions.Meta"/>, <see cref="McpServerPromptCreateOptions.Meta"/>,
65-
/// or <see cref="McpServerResourceCreateOptions.Meta"/> property to provide a JsonObject directly.
65+
/// For programmatic scenarios where you want to construct complex metadata without dealing with
66+
/// JSON strings, use the <see cref="McpServerToolCreateOptions.Meta"/>,
67+
/// <see cref="McpServerPromptCreateOptions.Meta"/>, or <see cref="McpServerResourceCreateOptions.Meta"/>
68+
/// property to provide a JsonObject directly.
6669
/// </para>
6770
/// </remarks>
68-
public object? Value { get; }
71+
public string Value { get; }
6972
}

tests/ModelContextProtocol.Tests/Server/McpMetaAttributeTests.cs

Lines changed: 18 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
using ModelContextProtocol.Server;
22
using System.Text.Json.Nodes;
3-
using System.Text.Json.Serialization;
43

54
namespace ModelContextProtocol.Tests.Server;
65

@@ -192,12 +191,11 @@ public void McpMetaAttribute_DuplicateKeys_WithSeedMeta_SeedTakesPrecedence()
192191
public void McpMetaAttribute_NonStringValues_Serialized()
193192
{
194193
var method = typeof(TestToolNonStringMetaClass).GetMethod(nameof(TestToolNonStringMetaClass.ToolWithNonStringMeta))!;
195-
var tool = McpServerTool.Create(method, target: null, options: new() { SerializerOptions = JsonContext5.Default.Options });
194+
var tool = McpServerTool.Create(method, target: null);
196195
Assert.NotNull(tool.ProtocolTool.Meta);
197196
Assert.Equal("42", tool.ProtocolTool.Meta["intValue"]?.ToString());
198-
Assert.Equal("true", tool.ProtocolTool.Meta["boolValue"]?.ToString());
199-
// Enum serialized as numeric by default
200-
Assert.Equal(((int)TestEnum.Second).ToString(), tool.ProtocolTool.Meta["enumValue"]?.ToString());
197+
Assert.Equal("True", tool.ProtocolTool.Meta["boolValue"]?.ToString());
198+
Assert.Equal("1", tool.ProtocolTool.Meta["enumValue"]?.ToString());
201199
}
202200

203201
[Fact]
@@ -237,8 +235,8 @@ public void McpMetaAttribute_DelegateOverload_PopulatesMeta()
237235
private class TestToolClass
238236
{
239237
[McpServerTool]
240-
[McpMeta("model", "gpt-4o")]
241-
[McpMeta("version", "1.0")]
238+
[McpMeta("model", "\"gpt-4o\"")]
239+
[McpMeta("version", "\"1.0\"")]
242240
public static string ToolWithMeta(string input)
243241
{
244242
return input;
@@ -251,7 +249,7 @@ public static string ToolWithoutMeta(string input)
251249
}
252250

253251
[McpServerTool]
254-
[McpMeta("test-key", "test-value")]
252+
[McpMeta("test-key", "\"test-value\"")]
255253
public static string ToolWithSingleMeta(string input)
256254
{
257255
return input;
@@ -261,8 +259,8 @@ public static string ToolWithSingleMeta(string input)
261259
private class TestPromptClass
262260
{
263261
[McpServerPrompt]
264-
[McpMeta("type", "reasoning")]
265-
[McpMeta("model", "claude-3")]
262+
[McpMeta("type", "\"reasoning\"")]
263+
[McpMeta("model", "\"claude-3\"")]
266264
public static string PromptWithMeta(string input)
267265
{
268266
return input;
@@ -272,8 +270,8 @@ public static string PromptWithMeta(string input)
272270
private class TestResourceClass
273271
{
274272
[McpServerResource(UriTemplate = "resource://test/{id}")]
275-
[McpMeta("encoding", "text/plain")]
276-
[McpMeta("caching", "cached")]
273+
[McpMeta("encoding", "\"text/plain\"")]
274+
[McpMeta("caching", "\"cached\"")]
277275
public static string ResourceWithMeta(string id)
278276
{
279277
return $"Resource content for {id}";
@@ -295,39 +293,32 @@ private class TestPromptNoMetaClass
295293
private class TestToolDuplicateMetaClass
296294
{
297295
[McpServerTool]
298-
[McpMeta("key", "first")]
299-
[McpMeta("key", "second")]
300-
[McpMeta("other", "other-value")]
296+
[McpMeta("key", "\"first\"")]
297+
[McpMeta("key", "\"second\"")]
298+
[McpMeta("other", "\"other-value\"")]
301299
public static string ToolWithDuplicateMeta(string input) => input;
302300
}
303301

304-
private enum TestEnum { First = 0, Second = 1 }
305-
306302
private class TestToolNonStringMetaClass
307303
{
308304
[McpServerTool]
309-
[McpMeta("intValue", 42)]
310-
[McpMeta("boolValue", true)]
311-
[McpMeta("enumValue", TestEnum.Second)]
305+
[McpMeta("intValue", "42")]
306+
[McpMeta("boolValue", "true")]
307+
[McpMeta("enumValue", "1")]
312308
public static string ToolWithNonStringMeta(string input) => input;
313309
}
314310

315311
private class TestToolNullMetaClass
316312
{
317313
[McpServerTool]
318-
[McpMeta("nullable", null)]
314+
[McpMeta("nullable", "null")]
319315
public static string ToolWithNullMeta(string input) => input;
320316
}
321317

322318
private class TestToolMethodMetaOnlyClass
323319
{
324320
[McpServerTool]
325-
[McpMeta("methodKey", "method")]
321+
[McpMeta("methodKey", "\"method\"")]
326322
public static string ToolWithMethodMeta(string input) => input;
327323
}
328-
329-
[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)]
330-
[JsonSerializable(typeof(string))]
331-
[JsonSerializable(typeof(TestEnum))]
332-
private partial class JsonContext5 : JsonSerializerContext;
333324
}

0 commit comments

Comments
 (0)