|
| 1 | +using ModelContextProtocol.Protocol; |
1 | 2 | using ModelContextProtocol.Server; |
2 | 3 | using System.Text.Json; |
3 | 4 | using System.Text.Json.Nodes; |
@@ -1246,6 +1247,177 @@ public void McpMetaAttribute_JsonValueForComplexTypes_SerializedCorrectly() |
1246 | 1247 | Assert.Equal("123", obj["num"]?.ToString()); |
1247 | 1248 | } |
1248 | 1249 |
|
| 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 | + |
1249 | 1421 | private class TestToolClass |
1250 | 1422 | { |
1251 | 1423 | [McpServerTool] |
@@ -1535,4 +1707,47 @@ private class TestResourceJsonValueMetaClass |
1535 | 1707 | [McpMeta("permissions", JsonValue = """["read", "write"]""")] |
1536 | 1708 | public static string ResourceWithJsonValueMeta(string id) => id; |
1537 | 1709 | } |
| 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 | + } |
1538 | 1753 | } |
0 commit comments