Skip to content

Commit d5be659

Browse files
Copilotstephentoub
andcommitted
Fix McpMeta propagation to Result types for tools, prompts, and resources
Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>
1 parent aa3ede2 commit d5be659

4 files changed

Lines changed: 236 additions & 6 deletions

File tree

src/ModelContextProtocol.Core/Server/AIFunctionMcpServerPrompt.cs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -212,9 +212,9 @@ public override async ValueTask<GetPromptResult> GetAsync(
212212

213213
object? result = await AIFunction.InvokeAsync(arguments, cancellationToken).ConfigureAwait(false);
214214

215-
return result switch
215+
GetPromptResult getPromptResult = result switch
216216
{
217-
GetPromptResult getPromptResult => getPromptResult,
217+
GetPromptResult gpr => gpr,
218218

219219
string text => new()
220220
{
@@ -250,5 +250,10 @@ public override async ValueTask<GetPromptResult> GetAsync(
250250

251251
_ => throw new InvalidOperationException($"Unknown result type '{result.GetType()}' returned from prompt function."),
252252
};
253+
254+
// Propagate metadata from the prompt to the result if the result doesn't already have metadata.
255+
getPromptResult.Meta ??= ProtocolPrompt.Meta;
256+
257+
return getPromptResult;
253258
}
254259
}

src/ModelContextProtocol.Core/Server/AIFunctionMcpServerResource.cs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -375,9 +375,9 @@ public override async ValueTask<ReadResourceResult> ReadAsync(
375375
object? result = await AIFunction.InvokeAsync(arguments, cancellationToken).ConfigureAwait(false);
376376

377377
// And process the result.
378-
return result switch
378+
ReadResourceResult readResourceResult = result switch
379379
{
380-
ReadResourceResult readResourceResult => readResourceResult,
380+
ReadResourceResult rrr => rrr,
381381

382382
ResourceContents content => new()
383383
{
@@ -441,5 +441,10 @@ public override async ValueTask<ReadResourceResult> ReadAsync(
441441

442442
_ => throw new InvalidOperationException($"Unsupported result type '{result.GetType()}' returned from resource function."),
443443
};
444+
445+
// Propagate metadata from the resource template to the result if the result doesn't already have metadata.
446+
readResourceResult.Meta ??= ProtocolResourceTemplate.Meta;
447+
448+
return readResourceResult;
444449
}
445450
}

src/ModelContextProtocol.Core/Server/AIFunctionMcpServerTool.cs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -268,7 +268,7 @@ public override async ValueTask<CallToolResult> InvokeAsync(
268268
result = await AIFunction.InvokeAsync(arguments, cancellationToken).ConfigureAwait(false);
269269

270270
JsonNode? structuredContent = CreateStructuredResponse(result);
271-
return result switch
271+
CallToolResult callToolResult = result switch
272272
{
273273
AIContent aiContent => new()
274274
{
@@ -303,14 +303,19 @@ public override async ValueTask<CallToolResult> InvokeAsync(
303303
StructuredContent = structuredContent,
304304
},
305305

306-
CallToolResult callToolResponse => callToolResponse,
306+
CallToolResult ctr => ctr,
307307

308308
_ => new()
309309
{
310310
Content = [new TextContentBlock { Text = JsonSerializer.Serialize(result, AIFunction.JsonSerializerOptions.GetTypeInfo(typeof(object))) }],
311311
StructuredContent = structuredContent,
312312
},
313313
};
314+
315+
// Propagate metadata from the tool to the result if the result doesn't already have metadata.
316+
callToolResult.Meta ??= ProtocolTool.Meta;
317+
318+
return callToolResult;
314319
}
315320

316321
/// <summary>Creates a name to use based on the supplied method and naming policy.</summary>

tests/ModelContextProtocol.Tests/Server/McpMetaAttributeTests.cs

Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using ModelContextProtocol.Protocol;
12
using ModelContextProtocol.Server;
23
using System.Text.Json;
34
using System.Text.Json.Nodes;
@@ -1246,6 +1247,177 @@ public void McpMetaAttribute_JsonValueForComplexTypes_SerializedCorrectly()
12461247
Assert.Equal("123", obj["num"]?.ToString());
12471248
}
12481249

1250+
#region Meta Propagation to Result Tests
1251+
1252+
[Fact]
1253+
public async Task McpServerTool_InvokeAsync_PropagatesMetaToResult()
1254+
{
1255+
var method = typeof(TestToolMetaPropagationClass).GetMethod(nameof(TestToolMetaPropagationClass.ToolWithMeta))!;
1256+
var tool = McpServerTool.Create(method, target: null);
1257+
1258+
// Verify tool has meta defined
1259+
Assert.NotNull(tool.ProtocolTool.Meta);
1260+
Assert.Equal("gpt-4o", tool.ProtocolTool.Meta["model"]?.ToString());
1261+
1262+
var result = await tool.InvokeAsync(
1263+
new RequestContext<CallToolRequestParams>(new Moq.Mock<McpServer>().Object, CreateTestJsonRpcRequest()) { Params = new() { Name = "tool_with_meta" } },
1264+
TestContext.Current.CancellationToken);
1265+
1266+
// Verify meta is propagated to result
1267+
Assert.NotNull(result.Meta);
1268+
Assert.Equal("gpt-4o", result.Meta["model"]?.ToString());
1269+
Assert.Equal("1.0", result.Meta["version"]?.ToString());
1270+
}
1271+
1272+
[Fact]
1273+
public async Task McpServerTool_InvokeAsync_DoesNotOverrideUserProvidedMeta()
1274+
{
1275+
var method = typeof(TestToolCallToolResultClass).GetMethod(nameof(TestToolCallToolResultClass.ToolReturnsCallToolResultWithMeta))!;
1276+
var tool = McpServerTool.Create(method, target: null);
1277+
1278+
// Verify tool has meta defined
1279+
Assert.NotNull(tool.ProtocolTool.Meta);
1280+
Assert.Equal("tool-meta-value", tool.ProtocolTool.Meta["toolMeta"]?.ToString());
1281+
1282+
var result = await tool.InvokeAsync(
1283+
new RequestContext<CallToolRequestParams>(new Moq.Mock<McpServer>().Object, CreateTestJsonRpcRequest()) { Params = new() { Name = "tool" } },
1284+
TestContext.Current.CancellationToken);
1285+
1286+
// Verify that user-provided meta is preserved (not overwritten by tool meta)
1287+
Assert.NotNull(result.Meta);
1288+
Assert.Equal("user-provided-meta-value", result.Meta["customMeta"]?.ToString());
1289+
Assert.False(result.Meta.ContainsKey("toolMeta"));
1290+
}
1291+
1292+
[Fact]
1293+
public async Task McpServerPrompt_GetAsync_PropagatesMetaToResult()
1294+
{
1295+
var method = typeof(TestPromptMetaPropagationClass).GetMethod(nameof(TestPromptMetaPropagationClass.PromptWithMeta))!;
1296+
var prompt = McpServerPrompt.Create(method, target: null);
1297+
1298+
// Verify prompt has meta defined
1299+
Assert.NotNull(prompt.ProtocolPrompt.Meta);
1300+
Assert.Equal("reasoning", prompt.ProtocolPrompt.Meta["type"]?.ToString());
1301+
1302+
var result = await prompt.GetAsync(
1303+
new RequestContext<GetPromptRequestParams>(new Moq.Mock<McpServer>().Object, CreateTestJsonRpcRequest()) { Params = new() { Name = "prompt" } },
1304+
TestContext.Current.CancellationToken);
1305+
1306+
// Verify meta is propagated to result
1307+
Assert.NotNull(result.Meta);
1308+
Assert.Equal("reasoning", result.Meta["type"]?.ToString());
1309+
Assert.Equal("claude-3", result.Meta["model"]?.ToString());
1310+
}
1311+
1312+
[Fact]
1313+
public async Task McpServerPrompt_GetAsync_DoesNotOverrideUserProvidedMeta()
1314+
{
1315+
var method = typeof(TestPromptGetPromptResultClass).GetMethod(nameof(TestPromptGetPromptResultClass.PromptReturnsGetPromptResultWithMeta))!;
1316+
var prompt = McpServerPrompt.Create(method, target: null);
1317+
1318+
// Verify prompt has meta defined
1319+
Assert.NotNull(prompt.ProtocolPrompt.Meta);
1320+
Assert.Equal("prompt-meta-value", prompt.ProtocolPrompt.Meta["promptMeta"]?.ToString());
1321+
1322+
var result = await prompt.GetAsync(
1323+
new RequestContext<GetPromptRequestParams>(new Moq.Mock<McpServer>().Object, CreateTestJsonRpcRequest()) { Params = new() { Name = "prompt" } },
1324+
TestContext.Current.CancellationToken);
1325+
1326+
// Verify that user-provided meta is preserved (not overwritten by prompt meta)
1327+
Assert.NotNull(result.Meta);
1328+
Assert.Equal("user-provided-meta-value", result.Meta["customMeta"]?.ToString());
1329+
Assert.False(result.Meta.ContainsKey("promptMeta"));
1330+
}
1331+
1332+
[Fact]
1333+
public async Task McpServerResource_ReadAsync_PropagatesMetaToResult()
1334+
{
1335+
var method = typeof(TestResourceClass).GetMethod(nameof(TestResourceClass.ResourceWithMeta))!;
1336+
var resource = McpServerResource.Create(method, target: null);
1337+
1338+
// Verify resource has meta defined
1339+
Assert.NotNull(resource.ProtocolResourceTemplate?.Meta);
1340+
Assert.Equal("text/plain", resource.ProtocolResourceTemplate.Meta["encoding"]?.ToString());
1341+
1342+
var result = await resource.ReadAsync(
1343+
new RequestContext<ReadResourceRequestParams>(new Moq.Mock<McpServer>().Object, CreateTestJsonRpcRequest()) { Params = new() { Uri = "resource://test/123" } },
1344+
TestContext.Current.CancellationToken);
1345+
1346+
// Verify meta is propagated to result
1347+
Assert.NotNull(result.Meta);
1348+
Assert.Equal("text/plain", result.Meta["encoding"]?.ToString());
1349+
Assert.Equal("cached", result.Meta["caching"]?.ToString());
1350+
}
1351+
1352+
[Fact]
1353+
public async Task McpServerResource_ReadAsync_DoesNotOverrideUserProvidedMeta()
1354+
{
1355+
var method = typeof(TestResourceReadResourceResultClass).GetMethod(nameof(TestResourceReadResourceResultClass.ResourceReturnsReadResourceResultWithMeta))!;
1356+
var resource = McpServerResource.Create(method, target: null);
1357+
1358+
// Verify resource has meta defined
1359+
Assert.NotNull(resource.ProtocolResourceTemplate?.Meta);
1360+
Assert.Equal("resource-meta-value", resource.ProtocolResourceTemplate.Meta["resourceMeta"]?.ToString());
1361+
1362+
var result = await resource.ReadAsync(
1363+
new RequestContext<ReadResourceRequestParams>(new Moq.Mock<McpServer>().Object, CreateTestJsonRpcRequest()) { Params = new() { Uri = "resource://test/123" } },
1364+
TestContext.Current.CancellationToken);
1365+
1366+
// Verify that user-provided meta is preserved (not overwritten by resource meta)
1367+
Assert.NotNull(result.Meta);
1368+
Assert.Equal("user-provided-meta-value", result.Meta["customMeta"]?.ToString());
1369+
Assert.False(result.Meta.ContainsKey("resourceMeta"));
1370+
}
1371+
1372+
[Fact]
1373+
public async Task McpServerTool_InvokeAsync_WithNoMeta_ResultHasNoMeta()
1374+
{
1375+
var method = typeof(TestToolMetaPropagationClass).GetMethod(nameof(TestToolMetaPropagationClass.ToolWithoutMeta))!;
1376+
var tool = McpServerTool.Create(method, target: null);
1377+
1378+
// Verify tool has no meta defined
1379+
Assert.Null(tool.ProtocolTool.Meta);
1380+
1381+
var result = await tool.InvokeAsync(
1382+
new RequestContext<CallToolRequestParams>(new Moq.Mock<McpServer>().Object, CreateTestJsonRpcRequest()) { Params = new() { Name = "tool_without_meta" } },
1383+
TestContext.Current.CancellationToken);
1384+
1385+
// Verify result has no meta
1386+
Assert.Null(result.Meta);
1387+
}
1388+
1389+
private static JsonRpcRequest CreateTestJsonRpcRequest()
1390+
{
1391+
return new JsonRpcRequest
1392+
{
1393+
Id = new RequestId("test-id"),
1394+
Method = "test/method",
1395+
Params = null
1396+
};
1397+
}
1398+
1399+
// Test classes specifically for result propagation tests (no parameters)
1400+
private class TestToolMetaPropagationClass
1401+
{
1402+
[McpServerTool]
1403+
[McpMeta("model", "gpt-4o")]
1404+
[McpMeta("version", "1.0")]
1405+
public static string ToolWithMeta() => "result";
1406+
1407+
[McpServerTool]
1408+
public static string ToolWithoutMeta() => "result";
1409+
}
1410+
1411+
private class TestPromptMetaPropagationClass
1412+
{
1413+
[McpServerPrompt]
1414+
[McpMeta("type", "reasoning")]
1415+
[McpMeta("model", "claude-3")]
1416+
public static string PromptWithMeta() => "result";
1417+
}
1418+
1419+
#endregion
1420+
12491421
private class TestToolClass
12501422
{
12511423
[McpServerTool]
@@ -1535,4 +1707,47 @@ private class TestResourceJsonValueMetaClass
15351707
[McpMeta("permissions", JsonValue = """["read", "write"]""")]
15361708
public static string ResourceWithJsonValueMeta(string id) => id;
15371709
}
1710+
1711+
// Test classes for user-provided result types with their own meta
1712+
private class TestToolCallToolResultClass
1713+
{
1714+
[McpServerTool]
1715+
[McpMeta("toolMeta", "tool-meta-value")]
1716+
public static CallToolResult ToolReturnsCallToolResultWithMeta()
1717+
{
1718+
return new CallToolResult
1719+
{
1720+
Content = [new TextContentBlock { Text = "test" }],
1721+
Meta = new JsonObject { ["customMeta"] = "user-provided-meta-value" }
1722+
};
1723+
}
1724+
}
1725+
1726+
private class TestPromptGetPromptResultClass
1727+
{
1728+
[McpServerPrompt]
1729+
[McpMeta("promptMeta", "prompt-meta-value")]
1730+
public static GetPromptResult PromptReturnsGetPromptResultWithMeta()
1731+
{
1732+
return new GetPromptResult
1733+
{
1734+
Messages = [new PromptMessage { Role = Role.User, Content = new TextContentBlock { Text = "test" } }],
1735+
Meta = new JsonObject { ["customMeta"] = "user-provided-meta-value" }
1736+
};
1737+
}
1738+
}
1739+
1740+
private class TestResourceReadResourceResultClass
1741+
{
1742+
[McpServerResource(UriTemplate = "resource://test/{id}")]
1743+
[McpMeta("resourceMeta", "resource-meta-value")]
1744+
public static ReadResourceResult ResourceReturnsReadResourceResultWithMeta(string id)
1745+
{
1746+
return new ReadResourceResult
1747+
{
1748+
Contents = [new TextResourceContents { Uri = $"resource://test/{id}", Text = id }],
1749+
Meta = new JsonObject { ["customMeta"] = "user-provided-meta-value" }
1750+
};
1751+
}
1752+
}
15381753
}

0 commit comments

Comments
 (0)