Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -212,9 +212,9 @@ public override async ValueTask<GetPromptResult> GetAsync(

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

return result switch
GetPromptResult getPromptResult = result switch
{
GetPromptResult getPromptResult => getPromptResult,
GetPromptResult existingResult => existingResult,

string text => new()
{
Expand Down Expand Up @@ -250,5 +250,10 @@ public override async ValueTask<GetPromptResult> GetAsync(

_ => throw new InvalidOperationException($"Unknown result type '{result.GetType()}' returned from prompt function."),
};

// Propagate metadata from the prompt to the result if the result doesn't already have metadata.
getPromptResult.Meta ??= ProtocolPrompt.Meta;

return getPromptResult;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -375,9 +375,9 @@ public override async ValueTask<ReadResourceResult> ReadAsync(
object? result = await AIFunction.InvokeAsync(arguments, cancellationToken).ConfigureAwait(false);

// And process the result.
return result switch
ReadResourceResult readResourceResult = result switch
{
ReadResourceResult readResourceResult => readResourceResult,
ReadResourceResult existingResult => existingResult,

ResourceContents content => new()
{
Expand Down Expand Up @@ -441,5 +441,10 @@ public override async ValueTask<ReadResourceResult> ReadAsync(

_ => throw new InvalidOperationException($"Unsupported result type '{result.GetType()}' returned from resource function."),
};

// Propagate metadata from the resource template to the result if the result doesn't already have metadata.
readResourceResult.Meta ??= ProtocolResourceTemplate.Meta;

return readResourceResult;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -268,7 +268,7 @@ public override async ValueTask<CallToolResult> InvokeAsync(
result = await AIFunction.InvokeAsync(arguments, cancellationToken).ConfigureAwait(false);

JsonNode? structuredContent = CreateStructuredResponse(result);
return result switch
CallToolResult callToolResult = result switch
{
AIContent aiContent => new()
{
Expand Down Expand Up @@ -303,14 +303,19 @@ public override async ValueTask<CallToolResult> InvokeAsync(
StructuredContent = structuredContent,
},

CallToolResult callToolResponse => callToolResponse,
CallToolResult existingResult => existingResult,

_ => new()
{
Content = [new TextContentBlock { Text = JsonSerializer.Serialize(result, AIFunction.JsonSerializerOptions.GetTypeInfo(typeof(object))) }],
StructuredContent = structuredContent,
},
};

// Propagate metadata from the tool to the result if the result doesn't already have metadata.
callToolResult.Meta ??= ProtocolTool.Meta;

return callToolResult;
}

/// <summary>Creates a name to use based on the supplied method and naming policy.</summary>
Expand Down
258 changes: 258 additions & 0 deletions tests/ModelContextProtocol.Tests/Server/McpMetaAttributeTests.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using ModelContextProtocol.Protocol;
using ModelContextProtocol.Server;
using System.Text.Json;
using System.Text.Json.Nodes;
Expand Down Expand Up @@ -1246,6 +1247,220 @@ public void McpMetaAttribute_JsonValueForComplexTypes_SerializedCorrectly()
Assert.Equal("123", obj["num"]?.ToString());
}

#region Meta Propagation to Result Tests

[Fact]
public async Task McpServerTool_InvokeAsync_PropagatesMetaToResult()
{
var method = typeof(TestToolMetaPropagationClass).GetMethod(nameof(TestToolMetaPropagationClass.ToolWithMeta))!;
var tool = McpServerTool.Create(method, target: null);

// Verify tool has meta defined
Assert.NotNull(tool.ProtocolTool.Meta);
Assert.Equal("gpt-4o", tool.ProtocolTool.Meta["model"]?.ToString());

var result = await tool.InvokeAsync(
new RequestContext<CallToolRequestParams>(new Moq.Mock<McpServer>().Object, CreateTestJsonRpcRequest()) { Params = new() { Name = "tool_with_meta" } },
TestContext.Current.CancellationToken);

// Verify meta is propagated to result
Assert.NotNull(result.Meta);
Assert.Equal("gpt-4o", result.Meta["model"]?.ToString());
Assert.Equal("1.0", result.Meta["version"]?.ToString());
}

[Fact]
public async Task McpServerTool_InvokeAsync_DoesNotOverrideUserProvidedMeta()
{
var method = typeof(TestToolCallToolResultClass).GetMethod(nameof(TestToolCallToolResultClass.ToolReturnsCallToolResultWithMeta))!;
var tool = McpServerTool.Create(method, target: null);

// Verify tool has meta defined
Assert.NotNull(tool.ProtocolTool.Meta);
Assert.Equal("tool-meta-value", tool.ProtocolTool.Meta["toolMeta"]?.ToString());

var result = await tool.InvokeAsync(
new RequestContext<CallToolRequestParams>(new Moq.Mock<McpServer>().Object, CreateTestJsonRpcRequest()) { Params = new() { Name = "tool" } },
TestContext.Current.CancellationToken);

// Verify that user-provided meta is preserved (not overwritten by tool meta)
Assert.NotNull(result.Meta);
Assert.Equal("user-provided-meta-value", result.Meta["customMeta"]?.ToString());
Assert.False(result.Meta.ContainsKey("toolMeta"));
}

[Fact]
public async Task McpServerPrompt_GetAsync_PropagatesMetaToResult()
{
var method = typeof(TestPromptMetaPropagationClass).GetMethod(nameof(TestPromptMetaPropagationClass.PromptWithMeta))!;
var prompt = McpServerPrompt.Create(method, target: null);

// Verify prompt has meta defined
Assert.NotNull(prompt.ProtocolPrompt.Meta);
Assert.Equal("reasoning", prompt.ProtocolPrompt.Meta["type"]?.ToString());

var result = await prompt.GetAsync(
new RequestContext<GetPromptRequestParams>(new Moq.Mock<McpServer>().Object, CreateTestJsonRpcRequest()) { Params = new() { Name = "prompt" } },
TestContext.Current.CancellationToken);

// Verify meta is propagated to result
Assert.NotNull(result.Meta);
Assert.Equal("reasoning", result.Meta["type"]?.ToString());
Assert.Equal("claude-3", result.Meta["model"]?.ToString());
}

[Fact]
public async Task McpServerPrompt_GetAsync_DoesNotOverrideUserProvidedMeta()
{
var method = typeof(TestPromptGetPromptResultClass).GetMethod(nameof(TestPromptGetPromptResultClass.PromptReturnsGetPromptResultWithMeta))!;
var prompt = McpServerPrompt.Create(method, target: null);

// Verify prompt has meta defined
Assert.NotNull(prompt.ProtocolPrompt.Meta);
Assert.Equal("prompt-meta-value", prompt.ProtocolPrompt.Meta["promptMeta"]?.ToString());

var result = await prompt.GetAsync(
new RequestContext<GetPromptRequestParams>(new Moq.Mock<McpServer>().Object, CreateTestJsonRpcRequest()) { Params = new() { Name = "prompt" } },
TestContext.Current.CancellationToken);

// Verify that user-provided meta is preserved (not overwritten by prompt meta)
Assert.NotNull(result.Meta);
Assert.Equal("user-provided-meta-value", result.Meta["customMeta"]?.ToString());
Assert.False(result.Meta.ContainsKey("promptMeta"));
}

[Fact]
public async Task McpServerResource_ReadAsync_PropagatesMetaToResult()
{
var method = typeof(TestResourceClass).GetMethod(nameof(TestResourceClass.ResourceWithMeta))!;
var resource = McpServerResource.Create(method, target: null);

// Verify resource has meta defined
Assert.NotNull(resource.ProtocolResourceTemplate?.Meta);
Assert.Equal("text/plain", resource.ProtocolResourceTemplate.Meta["encoding"]?.ToString());

var result = await resource.ReadAsync(
new RequestContext<ReadResourceRequestParams>(new Moq.Mock<McpServer>().Object, CreateTestJsonRpcRequest()) { Params = new() { Uri = "resource://test/123" } },
TestContext.Current.CancellationToken);

// Verify meta is propagated to result
Assert.NotNull(result.Meta);
Assert.Equal("text/plain", result.Meta["encoding"]?.ToString());
Assert.Equal("cached", result.Meta["caching"]?.ToString());
}

[Fact]
public async Task McpServerResource_ReadAsync_DoesNotOverrideUserProvidedMeta()
{
var method = typeof(TestResourceReadResourceResultClass).GetMethod(nameof(TestResourceReadResourceResultClass.ResourceReturnsReadResourceResultWithMeta))!;
var resource = McpServerResource.Create(method, target: null);

// Verify resource has meta defined
Assert.NotNull(resource.ProtocolResourceTemplate?.Meta);
Assert.Equal("resource-meta-value", resource.ProtocolResourceTemplate.Meta["resourceMeta"]?.ToString());

var result = await resource.ReadAsync(
new RequestContext<ReadResourceRequestParams>(new Moq.Mock<McpServer>().Object, CreateTestJsonRpcRequest()) { Params = new() { Uri = "resource://test/123" } },
TestContext.Current.CancellationToken);

// Verify that user-provided meta is preserved (not overwritten by resource meta)
Assert.NotNull(result.Meta);
Assert.Equal("user-provided-meta-value", result.Meta["customMeta"]?.ToString());
Assert.False(result.Meta.ContainsKey("resourceMeta"));
}

[Fact]
public async Task McpServerTool_InvokeAsync_WithNoMeta_ResultHasNoMeta()
{
var method = typeof(TestToolMetaPropagationClass).GetMethod(nameof(TestToolMetaPropagationClass.ToolWithoutMeta))!;
var tool = McpServerTool.Create(method, target: null);

// Verify tool has no meta defined
Assert.Null(tool.ProtocolTool.Meta);

var result = await tool.InvokeAsync(
new RequestContext<CallToolRequestParams>(new Moq.Mock<McpServer>().Object, CreateTestJsonRpcRequest()) { Params = new() { Name = "tool_without_meta" } },
TestContext.Current.CancellationToken);

// Verify result has no meta
Assert.Null(result.Meta);
}

[Fact]
public async Task McpServerPrompt_GetAsync_WithNoMeta_ResultHasNoMeta()
{
var method = typeof(TestPromptMetaPropagationClass).GetMethod(nameof(TestPromptMetaPropagationClass.PromptWithoutMeta))!;
var prompt = McpServerPrompt.Create(method, target: null);

// Verify prompt has no meta defined
Assert.Null(prompt.ProtocolPrompt.Meta);

var result = await prompt.GetAsync(
new RequestContext<GetPromptRequestParams>(new Moq.Mock<McpServer>().Object, CreateTestJsonRpcRequest()) { Params = new() { Name = "prompt_without_meta" } },
TestContext.Current.CancellationToken);

// Verify result has no meta
Assert.Null(result.Meta);
}

[Fact]
public async Task McpServerResource_ReadAsync_WithNoMeta_ResultHasNoMeta()
{
var method = typeof(TestResourceMetaPropagationClass).GetMethod(nameof(TestResourceMetaPropagationClass.ResourceWithoutMeta))!;
var resource = McpServerResource.Create(method, target: null);

// Verify resource has no meta defined
Assert.Null(resource.ProtocolResourceTemplate?.Meta);

var result = await resource.ReadAsync(
new RequestContext<ReadResourceRequestParams>(new Moq.Mock<McpServer>().Object, CreateTestJsonRpcRequest()) { Params = new() { Uri = "resource://no-meta/123" } },
TestContext.Current.CancellationToken);

// Verify result has no meta
Assert.Null(result.Meta);
}

private static JsonRpcRequest CreateTestJsonRpcRequest()
{
return new JsonRpcRequest
{
Id = new RequestId("test-id"),
Method = "test/method",
Params = null
};
}

// Test classes specifically for result propagation tests (no parameters)
private class TestToolMetaPropagationClass
{
[McpServerTool]
[McpMeta("model", "gpt-4o")]
[McpMeta("version", "1.0")]
public static string ToolWithMeta() => "result";

[McpServerTool]
public static string ToolWithoutMeta() => "result";
}

private class TestPromptMetaPropagationClass
{
[McpServerPrompt]
[McpMeta("type", "reasoning")]
[McpMeta("model", "claude-3")]
public static string PromptWithMeta() => "result";

[McpServerPrompt]
public static string PromptWithoutMeta() => "result";
}

private class TestResourceMetaPropagationClass
{
[McpServerResource(UriTemplate = "resource://no-meta/{id}")]
public static string ResourceWithoutMeta(string id) => id;
}

#endregion

private class TestToolClass
{
[McpServerTool]
Expand Down Expand Up @@ -1535,4 +1750,47 @@ private class TestResourceJsonValueMetaClass
[McpMeta("permissions", JsonValue = """["read", "write"]""")]
public static string ResourceWithJsonValueMeta(string id) => id;
}

// Test classes for user-provided result types with their own meta
private class TestToolCallToolResultClass
{
[McpServerTool]
[McpMeta("toolMeta", "tool-meta-value")]
public static CallToolResult ToolReturnsCallToolResultWithMeta()
{
return new CallToolResult
{
Content = [new TextContentBlock { Text = "test" }],
Meta = new JsonObject { ["customMeta"] = "user-provided-meta-value" }
};
}
}

private class TestPromptGetPromptResultClass
{
[McpServerPrompt]
[McpMeta("promptMeta", "prompt-meta-value")]
public static GetPromptResult PromptReturnsGetPromptResultWithMeta()
{
return new GetPromptResult
{
Messages = [new PromptMessage { Role = Role.User, Content = new TextContentBlock { Text = "test" } }],
Meta = new JsonObject { ["customMeta"] = "user-provided-meta-value" }
};
}
}

private class TestResourceReadResourceResultClass
{
[McpServerResource(UriTemplate = "resource://test/{id}")]
[McpMeta("resourceMeta", "resource-meta-value")]
public static ReadResourceResult ResourceReturnsReadResourceResultWithMeta(string id)
{
return new ReadResourceResult
{
Contents = [new TextResourceContents { Uri = $"resource://test/{id}", Text = id }],
Meta = new JsonObject { ["customMeta"] = "user-provided-meta-value" }
};
}
}
}