Skip to content

Commit c69cd96

Browse files
Copilotstephentoub
andcommitted
Add Meta property to options classes and update logic to merge with attributes
Co-authored-by: stephentoub <[email protected]>
1 parent d371818 commit c69cd96

File tree

7 files changed

+171
-9
lines changed

7 files changed

+171
-9
lines changed

src/ModelContextProtocol.Core/Server/AIFunctionMcpServerPrompt.cs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -138,10 +138,14 @@ private static AIFunctionFactoryOptions CreateAIFunctionFactoryOptions(
138138
Icons = options?.Icons,
139139
};
140140

141-
// Populate Meta from McpMetaAttribute instances if a MethodInfo is in the metadata
141+
// Populate Meta from options and/or McpMetaAttribute instances if a MethodInfo is in the metadata
142142
if (options?.Metadata?.FirstOrDefault(m => m is MethodInfo) is MethodInfo method)
143143
{
144-
prompt.Meta = AIFunctionMcpServerTool.CreateMetaFromAttributes(method);
144+
prompt.Meta = AIFunctionMcpServerTool.CreateMetaFromAttributes(method, options.Meta);
145+
}
146+
else if (options?.Meta is not null)
147+
{
148+
prompt.Meta = options.Meta;
145149
}
146150

147151
return new AIFunctionMcpServerPrompt(function, prompt, options?.Metadata ?? []);

src/ModelContextProtocol.Core/Server/AIFunctionMcpServerResource.cs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -211,11 +211,15 @@ private static AIFunctionFactoryOptions CreateAIFunctionFactoryOptions(
211211

212212
string name = options?.Name ?? function.Name;
213213

214-
// Populate Meta from McpMetaAttribute instances if a MethodInfo is in the metadata
214+
// Populate Meta from options and/or McpMetaAttribute instances if a MethodInfo is in the metadata
215215
JsonObject? meta = null;
216216
if (options?.Metadata?.FirstOrDefault(m => m is MethodInfo) is MethodInfo method)
217217
{
218-
meta = AIFunctionMcpServerTool.CreateMetaFromAttributes(method);
218+
meta = AIFunctionMcpServerTool.CreateMetaFromAttributes(method, options.Meta);
219+
}
220+
else if (options?.Meta is not null)
221+
{
222+
meta = options.Meta;
219223
}
220224

221225
ResourceTemplate resource = new()

src/ModelContextProtocol.Core/Server/AIFunctionMcpServerTool.cs

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -144,10 +144,14 @@ options.OpenWorld is not null ||
144144
};
145145
}
146146

147-
// Populate Meta from McpMetaAttribute instances if a MethodInfo is in the metadata
147+
// Populate Meta from options and/or McpMetaAttribute instances if a MethodInfo is in the metadata
148148
if (options.Metadata?.FirstOrDefault(m => m is MethodInfo) is MethodInfo method)
149149
{
150-
tool.Meta = CreateMetaFromAttributes(method);
150+
tool.Meta = CreateMetaFromAttributes(method, options.Meta);
151+
}
152+
else if (options.Meta is not null)
153+
{
154+
tool.Meta = options.Meta;
151155
}
152156
}
153157

@@ -358,16 +362,23 @@ internal static IReadOnlyList<object> CreateMetadata(MethodInfo method)
358362
}
359363

360364
/// <summary>Creates a Meta JsonObject from McpMetaAttribute instances on the specified method.</summary>
361-
internal static JsonObject? CreateMetaFromAttributes(MethodInfo method)
365+
/// <param name="method">The method to extract McpMetaAttribute instances from.</param>
366+
/// <param name="seedMeta">Optional JsonObject to seed the Meta with. Properties from this object take precedence over attributes.</param>
367+
/// <returns>A JsonObject with metadata, or null if no metadata is present.</returns>
368+
internal static JsonObject? CreateMetaFromAttributes(MethodInfo method, JsonObject? seedMeta = null)
362369
{
363370
// Get all McpMetaAttribute instances from the method
364371
var metaAttributes = method.GetCustomAttributes<McpMetaAttribute>();
365372

366-
JsonObject? meta = null;
373+
JsonObject? meta = seedMeta;
367374
foreach (var attr in metaAttributes)
368375
{
369376
meta ??= new JsonObject();
370-
meta[attr.Name] = attr.Value;
377+
// Only add the attribute property if it doesn't already exist in the seed
378+
if (!meta.ContainsKey(attr.Name))
379+
{
380+
meta[attr.Name] = attr.Value;
381+
}
371382
}
372383

373384
return meta;

src/ModelContextProtocol.Core/Server/McpServerPromptCreateOptions.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,21 @@ public sealed class McpServerPromptCreateOptions
8686
/// </remarks>
8787
public IList<Icon>? Icons { get; set; }
8888

89+
/// <summary>
90+
/// Gets or sets metadata reserved by MCP for protocol-level metadata.
91+
/// </summary>
92+
/// <remarks>
93+
/// <para>
94+
/// This JsonObject is used to seed the <see cref="Prompt.Meta"/> property. Any metadata from
95+
/// <see cref="McpMetaAttribute"/> instances on the method will be added to this object, but
96+
/// properties already present in this JsonObject will not be overwritten.
97+
/// </para>
98+
/// <para>
99+
/// Implementations must not make assumptions about its contents.
100+
/// </para>
101+
/// </remarks>
102+
public System.Text.Json.Nodes.JsonObject? Meta { get; set; }
103+
89104
/// <summary>
90105
/// Creates a shallow clone of the current <see cref="McpServerPromptCreateOptions"/> instance.
91106
/// </summary>
@@ -100,5 +115,6 @@ internal McpServerPromptCreateOptions Clone() =>
100115
SchemaCreateOptions = SchemaCreateOptions,
101116
Metadata = Metadata,
102117
Icons = Icons,
118+
Meta = Meta,
103119
};
104120
}

src/ModelContextProtocol.Core/Server/McpServerResourceCreateOptions.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,21 @@ public sealed class McpServerResourceCreateOptions
101101
/// </remarks>
102102
public IList<Icon>? Icons { get; set; }
103103

104+
/// <summary>
105+
/// Gets or sets metadata reserved by MCP for protocol-level metadata.
106+
/// </summary>
107+
/// <remarks>
108+
/// <para>
109+
/// This JsonObject is used to seed the <see cref="Resource.Meta"/> property. Any metadata from
110+
/// <see cref="McpMetaAttribute"/> instances on the method will be added to this object, but
111+
/// properties already present in this JsonObject will not be overwritten.
112+
/// </para>
113+
/// <para>
114+
/// Implementations must not make assumptions about its contents.
115+
/// </para>
116+
/// </remarks>
117+
public System.Text.Json.Nodes.JsonObject? Meta { get; set; }
118+
104119
/// <summary>
105120
/// Creates a shallow clone of the current <see cref="McpServerResourceCreateOptions"/> instance.
106121
/// </summary>
@@ -117,5 +132,6 @@ internal McpServerResourceCreateOptions Clone() =>
117132
SchemaCreateOptions = SchemaCreateOptions,
118133
Metadata = Metadata,
119134
Icons = Icons,
135+
Meta = Meta,
120136
};
121137
}

src/ModelContextProtocol.Core/Server/McpServerToolCreateOptions.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,21 @@ public sealed class McpServerToolCreateOptions
172172
/// </remarks>
173173
public IList<Icon>? Icons { get; set; }
174174

175+
/// <summary>
176+
/// Gets or sets metadata reserved by MCP for protocol-level metadata.
177+
/// </summary>
178+
/// <remarks>
179+
/// <para>
180+
/// This JsonObject is used to seed the <see cref="Tool.Meta"/> property. Any metadata from
181+
/// <see cref="McpMetaAttribute"/> instances on the method will be added to this object, but
182+
/// properties already present in this JsonObject will not be overwritten.
183+
/// </para>
184+
/// <para>
185+
/// Implementations must not make assumptions about its contents.
186+
/// </para>
187+
/// </remarks>
188+
public System.Text.Json.Nodes.JsonObject? Meta { get; set; }
189+
175190
/// <summary>
176191
/// Creates a shallow clone of the current <see cref="McpServerToolCreateOptions"/> instance.
177192
/// </summary>
@@ -191,5 +206,6 @@ internal McpServerToolCreateOptions Clone() =>
191206
SchemaCreateOptions = SchemaCreateOptions,
192207
Metadata = Metadata,
193208
Icons = Icons,
209+
Meta = Meta,
194210
};
195211
}

tests/ModelContextProtocol.Tests/Server/McpMetaAttributeTests.cs

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,101 @@ public void McpMetaAttribute_SingleAttribute_PopulatesMeta()
8080
Assert.Single(tool.ProtocolTool.Meta);
8181
}
8282

83+
[Fact]
84+
public void McpMetaAttribute_OptionsMetaTakesPrecedence()
85+
{
86+
// Arrange
87+
var method = typeof(TestToolClass).GetMethod(nameof(TestToolClass.ToolWithMeta))!;
88+
var seedMeta = new JsonObject
89+
{
90+
["model"] = "options-model",
91+
["extra"] = "options-extra"
92+
};
93+
var options = new McpServerToolCreateOptions { Meta = seedMeta };
94+
95+
// Act
96+
var tool = McpServerTool.Create(method, options);
97+
98+
// Assert
99+
Assert.NotNull(tool.ProtocolTool.Meta);
100+
// Options Meta should win for "model"
101+
Assert.Equal("options-model", tool.ProtocolTool.Meta["model"]?.ToString());
102+
// Attribute should add "version" since it's not in options
103+
Assert.Equal("1.0", tool.ProtocolTool.Meta["version"]?.ToString());
104+
// Options Meta should include "extra"
105+
Assert.Equal("options-extra", tool.ProtocolTool.Meta["extra"]?.ToString());
106+
}
107+
108+
[Fact]
109+
public void McpMetaAttribute_OptionsMetaOnly_NoAttributes()
110+
{
111+
// Arrange
112+
var method = typeof(TestToolClass).GetMethod(nameof(TestToolClass.ToolWithoutMeta))!;
113+
var seedMeta = new JsonObject
114+
{
115+
["custom"] = "value"
116+
};
117+
var options = new McpServerToolCreateOptions { Meta = seedMeta };
118+
119+
// Act
120+
var tool = McpServerTool.Create(method, options);
121+
122+
// Assert
123+
Assert.NotNull(tool.ProtocolTool.Meta);
124+
Assert.Equal("value", tool.ProtocolTool.Meta["custom"]?.ToString());
125+
Assert.Single(tool.ProtocolTool.Meta);
126+
}
127+
128+
[Fact]
129+
public void McpMetaAttribute_PromptOptionsMetaTakesPrecedence()
130+
{
131+
// Arrange
132+
var method = typeof(TestPromptClass).GetMethod(nameof(TestPromptClass.PromptWithMeta))!;
133+
var seedMeta = new JsonObject
134+
{
135+
["type"] = "options-type",
136+
["extra"] = "options-extra"
137+
};
138+
var options = new McpServerPromptCreateOptions { Meta = seedMeta };
139+
140+
// Act
141+
var prompt = McpServerPrompt.Create(method, options);
142+
143+
// Assert
144+
Assert.NotNull(prompt.ProtocolPrompt.Meta);
145+
// Options Meta should win for "type"
146+
Assert.Equal("options-type", prompt.ProtocolPrompt.Meta["type"]?.ToString());
147+
// Attribute should add "model" since it's not in options
148+
Assert.Equal("claude-3", prompt.ProtocolPrompt.Meta["model"]?.ToString());
149+
// Options Meta should include "extra"
150+
Assert.Equal("options-extra", prompt.ProtocolPrompt.Meta["extra"]?.ToString());
151+
}
152+
153+
[Fact]
154+
public void McpMetaAttribute_ResourceOptionsMetaTakesPrecedence()
155+
{
156+
// Arrange
157+
var method = typeof(TestResourceClass).GetMethod(nameof(TestResourceClass.ResourceWithMeta))!;
158+
var seedMeta = new JsonObject
159+
{
160+
["encoding"] = "options-encoding",
161+
["extra"] = "options-extra"
162+
};
163+
var options = new McpServerResourceCreateOptions { Meta = seedMeta };
164+
165+
// Act
166+
var resource = McpServerResource.Create(method, options);
167+
168+
// Assert
169+
Assert.NotNull(resource.ProtocolResource.Meta);
170+
// Options Meta should win for "encoding"
171+
Assert.Equal("options-encoding", resource.ProtocolResource.Meta["encoding"]?.ToString());
172+
// Attribute should add "caching" since it's not in options
173+
Assert.Equal("cached", resource.ProtocolResource.Meta["caching"]?.ToString());
174+
// Options Meta should include "extra"
175+
Assert.Equal("options-extra", resource.ProtocolResource.Meta["extra"]?.ToString());
176+
}
177+
83178
private class TestToolClass
84179
{
85180
[McpServerTool]

0 commit comments

Comments
 (0)