Update content according to latest spec (#513)

* Update content according to latest spec

* Add Title to a bunch of types

* Add _meta to lots of types

* Address feedback, more cleanup, fix NAOT test failure
This commit is contained in:
Stephen Toub 2025-06-16 19:39:41 -04:00 • committed by GitHub
commit 2bcff315f8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
27 changed files with 1700 additions and 853 deletions

View file

@ -12,7 +12,7 @@ public class TestServerTransport : ITransport
public ChannelReader<JsonRpcMessage> MessageReader => _messageChannel;
public List<JsonRpcMessage> SentMessages { get; } = [];
public IList<JsonRpcMessage> SentMessages { get; } = [];
public Action<JsonRpcMessage>? OnMessageSent { get; set; }
@ -74,7 +74,7 @@ public class TestServerTransport : ITransport
await WriteMessageAsync(new JsonRpcResponse
{
Id = request.Id,
Result = JsonSerializer.SerializeToNode(new CreateMessageResult { Content = new(), Model = "model", Role = Role.User }, McpJsonUtilities.DefaultOptions),
Result = JsonSerializer.SerializeToNode(new CreateMessageResult { Content = new TextContentBlock { Text = "" }, Model = "model", Role = Role.User }, McpJsonUtilities.DefaultOptions),
}, cancellationToken);
}

View file

@ -95,7 +95,7 @@ public abstract class HttpServerIntegrationTests : LoggedTest, IClassFixture<Sse
// assert
Assert.NotNull(result);
Assert.False(result.IsError);
var textContent = Assert.Single(result.Content, c => c.Type == "text");
var textContent = Assert.Single(result.Content.OfType<TextContentBlock>());
Assert.Equal("Echo: Hello MCP!", textContent.Text);
}
@ -119,9 +119,9 @@ public abstract class HttpServerIntegrationTests : LoggedTest, IClassFixture<Sse
Assert.False(result2.IsError);
Assert.False(result3.IsError);
var textContent1 = Assert.Single(result1.Content);
var textContent2 = Assert.Single(result2.Content);
var textContent3 = Assert.Single(result3.Content);
var textContent1 = Assert.Single(result1.Content.OfType<TextContentBlock>());
var textContent2 = Assert.Single(result2.Content.OfType<TextContentBlock>());
var textContent3 = Assert.Single(result3.Content.OfType<TextContentBlock>());
Assert.NotNull(textContent1.Text);
Assert.Equal(textContent1.Text, textContent2.Text);
@ -259,11 +259,7 @@ public abstract class HttpServerIntegrationTests : LoggedTest, IClassFixture<Sse
{
Model = "test-model",
Role = Role.Assistant,
Content = new Content
{
Type = "text",
Text = "Test response"
}
Content = new TextContentBlock { Text = "Test response" },
};
};
await using var client = await GetClientAsync(options);
@ -279,8 +275,7 @@ public abstract class HttpServerIntegrationTests : LoggedTest, IClassFixture<Sse
// assert
Assert.NotNull(result);
var textContent = Assert.Single(result.Content);
Assert.Equal("text", textContent.Type);
var textContent = Assert.Single(result.Content.OfType<TextContentBlock>());
Assert.False(string.IsNullOrEmpty(textContent.Text));
}
@ -304,7 +299,7 @@ public abstract class HttpServerIntegrationTests : LoggedTest, IClassFixture<Sse
Assert.NotNull(result);
Assert.False(result.IsError);
var textContent = Assert.Single(result.Content, c => c.Type == "text");
var textContent = Assert.Single(result.Content.OfType<TextContentBlock>());
Assert.Equal($"Echo: Hello MCP! {i}", textContent.Text);
}
}

View file

@ -84,7 +84,7 @@ public abstract class MapMcpTests(ITestOutputHelper testOutputHelper) : KestrelI
new Dictionary<string, object?>() { ["message"] = "Hello world!" },
cancellationToken: TestContext.Current.CancellationToken);
var content = Assert.Single(response.Content);
var content = Assert.Single(response.Content.OfType<TextContentBlock>());
Assert.Equal("TestUser: Hello world!", content.Text);
}
@ -149,19 +149,14 @@ public abstract class MapMcpTests(ITestOutputHelper testOutputHelper) : KestrelI
Assert.NotNull(parameters?.Messages);
var message = Assert.Single(parameters.Messages);
Assert.Equal(Role.User, message.Role);
Assert.Equal("text", message.Content.Type);
Assert.Equal("Test prompt for sampling", message.Content.Text);
Assert.Equal("Test prompt for sampling", Assert.IsType<TextContentBlock>(message.Content).Text);
sampleCount++;
return new CreateMessageResult
{
Model = "test-model",
Role = Role.Assistant,
Content = new Content
{
Type = "text",
Text = "Sampling response from client"
}
Content = new TextContentBlock { Text = "Sampling response from client" },
};
},
},
@ -179,7 +174,7 @@ public abstract class MapMcpTests(ITestOutputHelper testOutputHelper) : KestrelI
Assert.False(result.IsError);
var textContent = Assert.Single(result.Content);
Assert.Equal("text", textContent.Type);
Assert.Equal("Sampling completed successfully. Client responded: Sampling response from client", textContent.Text);
Assert.Equal("Sampling completed successfully. Client responded: Sampling response from client", Assert.IsType<TextContentBlock>(textContent).Text);
Assert.Equal(2, sampleCount);
@ -228,11 +223,7 @@ public abstract class MapMcpTests(ITestOutputHelper testOutputHelper) : KestrelI
new SamplingMessage
{
Role = Role.User,
Content = new Content
{
Type = "text",
Text = prompt
},
Content = new TextContentBlock { Text = prompt },
}
],
};
@ -240,7 +231,7 @@ public abstract class MapMcpTests(ITestOutputHelper testOutputHelper) : KestrelI
await server.SampleAsync(samplingRequest, cancellationToken);
var samplingResult = await server.SampleAsync(samplingRequest, cancellationToken);
return $"Sampling completed successfully. Client responded: {samplingResult.Content.Text}";
return $"Sampling completed successfully. Client responded: {Assert.IsType<TextContentBlock>(samplingResult.Content).Text}";
}
}
}

View file

@ -159,7 +159,7 @@ public partial class SseIntegrationTests(ITestOutputHelper outputHelper) : Kestr
["message"] = "from client!"
},
cancellationToken: TestContext.Current.CancellationToken);
var textContent = Assert.Single(echoResponse.Content, c => c.Type == "text");
var textContent = Assert.Single(echoResponse.Content.OfType<TextContentBlock>());
Assert.Equal("hello from client!", textContent.Text);
}
@ -199,7 +199,7 @@ public partial class SseIntegrationTests(ITestOutputHelper outputHelper) : Kestr
{
Endpoint = new Uri("http://localhost/sse"),
Name = "In-memory SSE Client",
AdditionalHeaders = new()
AdditionalHeaders = new Dictionary<string, string>
{
["Authorize"] = "Bearer testToken"
},
@ -226,7 +226,7 @@ public partial class SseIntegrationTests(ITestOutputHelper outputHelper) : Kestr
{
Endpoint = new Uri("http://localhost/sse"),
Name = "In-memory SSE Client",
AdditionalHeaders = new()
AdditionalHeaders = new Dictionary<string, string>()
{
[""] = ""
},

View file

@ -113,7 +113,7 @@ public class StatelessServerTests(ITestOutputHelper outputHelper) : KestrelInMem
var toolResponse = await client.CallToolAsync("testSamplingErrors", cancellationToken: TestContext.Current.CancellationToken);
var toolContent = Assert.Single(toolResponse.Content);
Assert.Equal("Server to client requests are not supported in stateless mode.", toolContent.Text);
Assert.Equal("Server to client requests are not supported in stateless mode.", Assert.IsType<TextContentBlock>(toolContent).Text);
}
[Fact]
@ -133,7 +133,7 @@ public class StatelessServerTests(ITestOutputHelper outputHelper) : KestrelInMem
var toolResponse = await client.CallToolAsync("testRootsErrors", cancellationToken: TestContext.Current.CancellationToken);
var toolContent = Assert.Single(toolResponse.Content);
Assert.Equal("Server to client requests are not supported in stateless mode.", toolContent.Text);
Assert.Equal("Server to client requests are not supported in stateless mode.", Assert.IsType<TextContentBlock>(toolContent).Text);
}
[Fact]
@ -153,7 +153,7 @@ public class StatelessServerTests(ITestOutputHelper outputHelper) : KestrelInMem
var toolResponse = await client.CallToolAsync("testElicitationErrors", cancellationToken: TestContext.Current.CancellationToken);
var toolContent = Assert.Single(toolResponse.Content);
Assert.Equal("Server to client requests are not supported in stateless mode.", toolContent.Text);
Assert.Equal("Server to client requests are not supported in stateless mode.", Assert.IsType<TextContentBlock>(toolContent).Text);
}
[Fact]
@ -190,7 +190,7 @@ public class StatelessServerTests(ITestOutputHelper outputHelper) : KestrelInMem
var toolResponse = await client.CallToolAsync("testScope", cancellationToken: TestContext.Current.CancellationToken);
var toolContent = Assert.Single(toolResponse.Content);
Assert.Equal("From request middleware!", toolContent.Text);
Assert.Equal("From request middleware!", Assert.IsType<TextContentBlock>(toolContent).Text);
}
[McpServerTool(Name = "testSamplingErrors")]

View file

@ -77,9 +77,9 @@ public class StreamableHttpClientConformanceTests(ITestOutputHelper outputHelper
return Results.Json(new JsonRpcResponse
{
Id = request.Id,
Result = JsonSerializer.SerializeToNode(new CallToolResponse()
Result = JsonSerializer.SerializeToNode(new CallToolResult()
{
Content = [new() { Text = parameters.Arguments["message"].ToString() }],
Content = [new TextContentBlock { Text = parameters.Arguments["message"].ToString() }],
}, McpJsonUtilities.DefaultOptions),
});
}
@ -141,8 +141,7 @@ public class StreamableHttpClientConformanceTests(ITestOutputHelper outputHelper
var response = await echoTool.CallAsync(new Dictionary<string, object?>() { ["message"] = "Hello world!" }, cancellationToken: TestContext.Current.CancellationToken);
Assert.NotNull(response);
var content = Assert.Single(response.Content);
Assert.Equal("text", content.Type);
Assert.Equal("Hello world!", content.Text);
Assert.Equal("Hello world!", Assert.IsType<TextContentBlock>(content).Text);
}
public async ValueTask DisposeAsync()

View file

@ -218,9 +218,9 @@ public class StreamableHttpServerConformanceTests(ITestOutputHelper outputHelper
var response = await HttpClient.PostAsync("", JsonContent(CallTool("throw")), TestContext.Current.CancellationToken);
var rpcError = await AssertSingleSseResponseAsync(response);
var error = AssertType<CallToolResponse>(rpcError.Result);
var error = AssertType<CallToolResult>(rpcError.Result);
var content = Assert.Single(error.Content);
Assert.Contains("'throw'", content.Text);
Assert.Contains("'throw'", Assert.IsType<TextContentBlock>(content).Text);
}
[Fact]
@ -371,16 +371,15 @@ public class StreamableHttpServerConformanceTests(ITestOutputHelper outputHelper
if (currentSseItem <= 10)
{
var notification = JsonSerializer.Deserialize(sseEvent, GetJsonTypeInfo<JsonRpcNotification>());
var progressNotification = AssertType<ProgressNotification>(notification?.Params);
var progressNotification = AssertType<ProgressNotificationParams>(notification?.Params);
Assert.Equal($"Progress {currentSseItem - 1}", progressNotification.Progress.Message);
}
else
{
var rpcResponse = JsonSerializer.Deserialize(sseEvent, GetJsonTypeInfo<JsonRpcResponse>());
var callToolResponse = AssertType<CallToolResponse>(rpcResponse?.Result);
var callToolResponse = AssertType<CallToolResult>(rpcResponse?.Result);
var callToolContent = Assert.Single(callToolResponse.Content);
Assert.Equal("text", callToolContent.Type);
Assert.Equal("done", callToolContent.Text);
Assert.Equal("done", Assert.IsType<TextContentBlock>(callToolContent).Text);
}
}
@ -413,10 +412,9 @@ public class StreamableHttpServerConformanceTests(ITestOutputHelper outputHelper
{
var response = await HttpClient.PostAsync("", JsonContent(CallTool("async-local-session")), TestContext.Current.CancellationToken);
var rpcResponse = await AssertSingleSseResponseAsync(response);
var callToolResponse = AssertType<CallToolResponse>(rpcResponse.Result);
var callToolResponse = AssertType<CallToolResult>(rpcResponse.Result);
var callToolContent = Assert.Single(callToolResponse.Content);
Assert.Equal("text", callToolContent.Type);
Assert.Equal($"RunSessionHandler ({expectedSessionIndex})", callToolContent.Text);
Assert.Equal($"RunSessionHandler ({expectedSessionIndex})", Assert.IsType<TextContentBlock>(callToolContent).Text);
}
await CallAsyncLocalToolAndValidateAsync(expectedSessionIndex: 0);
@ -527,7 +525,7 @@ public class StreamableHttpServerConformanceTests(ITestOutputHelper outputHelper
private static T AssertType<T>(JsonNode? jsonNode)
{
var type = JsonSerializer.Deserialize<T>(jsonNode, GetJsonTypeInfo<T>());
var type = JsonSerializer.Deserialize(jsonNode, GetJsonTypeInfo<T>());
Assert.NotNull(type);
return type;
}
@ -603,12 +601,11 @@ public class StreamableHttpServerConformanceTests(ITestOutputHelper outputHelper
return initializeResult;
}
private static CallToolResponse AssertEchoResponse(JsonRpcResponse rpcResponse)
private static CallToolResult AssertEchoResponse(JsonRpcResponse rpcResponse)
{
var callToolResponse = AssertType<CallToolResponse>(rpcResponse.Result);
var callToolResponse = AssertType<CallToolResult>(rpcResponse.Result);
var callToolContent = Assert.Single(callToolResponse.Content);
Assert.Equal("text", callToolContent.Type);
Assert.Equal($"Hello world! ({rpcResponse.Id})", callToolContent.Text);
Assert.Equal($"Hello world! ({rpcResponse.Id})", Assert.IsType<TextContentBlock>(callToolContent).Text);
return callToolResponse;
}

View file

@ -175,16 +175,16 @@ internal static class Program
{
throw new McpException("Missing required argument 'message'", McpErrorCode.InvalidParams);
}
return new CallToolResponse()
return new CallToolResult()
{
Content = [new Content() { Text = "Echo: " + message.ToString(), Type = "text" }]
Content = [new TextContentBlock { Text = $"Echo: {message}" }]
};
}
else if (request.Params?.Name == "echoSessionId")
{
return new CallToolResponse()
return new CallToolResult()
{
Content = [new Content() { Text = request.Server.SessionId, Type = "text" }]
Content = [new TextContentBlock { Text = request.Server.SessionId ?? string.Empty }]
};
}
else if (request.Params?.Name == "sampleLLM")
@ -198,9 +198,9 @@ internal static class Program
var sampleResult = await request.Server.SampleAsync(CreateRequestSamplingParams(prompt.ToString(), "sampleLLM", Convert.ToInt32(maxTokens.GetRawText())),
cancellationToken);
return new CallToolResponse()
return new CallToolResult()
{
Content = [new Content() { Text = $"LLM sampling result: {sampleResult.Content.Text}", Type = "text" }]
Content = [new TextContentBlock { Text = $"LLM sampling result: {(sampleResult.Content as TextContentBlock)?.Text}" }]
};
}
else
@ -257,11 +257,7 @@ internal static class Program
messages.Add(new PromptMessage()
{
Role = Role.User,
Content = new Content()
{
Type = "text",
Text = "This is a simple prompt without arguments."
}
Content = new TextContentBlock { Text = "This is a simple prompt without arguments." },
});
}
else if (request.Params?.Name == "complex_prompt")
@ -271,27 +267,18 @@ internal static class Program
messages.Add(new PromptMessage()
{
Role = Role.User,
Content = new Content()
{
Type = "text",
Text = $"This is a complex prompt with arguments: temperature={temperature}, style={style}"
}
Content = new TextContentBlock { Text = $"This is a complex prompt with arguments: temperature={temperature}, style={style}" },
});
messages.Add(new PromptMessage()
{
Role = Role.Assistant,
Content = new Content()
{
Type = "text",
Text = "I understand. You've provided a complex prompt with temperature and style arguments. How would you like me to proceed?"
}
Content = new TextContentBlock { Text = "I understand. You've provided a complex prompt with temperature and style arguments. How would you like me to proceed?" },
});
messages.Add(new PromptMessage()
{
Role = Role.User,
Content = new Content()
Content = new ImageContentBlock()
{
Type = "image",
Data = MCP_TINY_IMAGE,
MimeType = "image/png"
}
@ -506,29 +493,29 @@ internal static class Program
Func<RequestContext<CompleteRequestParams>, CancellationToken, ValueTask<CompleteResult>> handler = async (request, cancellationToken) =>
{
if (request.Params?.Ref?.Type == "ref/resource")
string[]? values;
switch (request.Params?.Ref)
{
var resourceId = request.Params?.Ref?.Uri?.Split('/').LastOrDefault();
if (string.IsNullOrEmpty(resourceId))
return new CompleteResult() { Completion = new() { Values = [] } };
case ResourceTemplateReference rtr:
var resourceId = rtr.Uri?.Split('/').LastOrDefault();
if (string.IsNullOrEmpty(resourceId))
return new CompleteResult { Completion = new() { Values = [] } };
// Filter resource IDs that start with the input value
var values = sampleResourceIds.Where(id => id.StartsWith(request.Params!.Argument.Value)).ToArray();
return new CompleteResult() { Completion = new() { Values = values, HasMore = false, Total = values.Length } };
// Filter resource IDs that start with the input value
values = sampleResourceIds.Where(id => id.StartsWith(request.Params!.Argument.Value)).ToArray();
return new CompleteResult { Completion = new() { Values = values, HasMore = false, Total = values.Length } };
case PromptReference pr:
// Handle completion for prompt arguments
if (!exampleCompletions.TryGetValue(request.Params.Argument.Name, out var completions))
return new CompleteResult { Completion = new() { Values = [] } };
values = completions.Where(value => value.StartsWith(request.Params.Argument.Value)).ToArray();
return new CompleteResult { Completion = new() { Values = values, HasMore = false, Total = values.Length } };
default:
throw new McpException($"Unknown reference type: '{request.Params?.Ref.Type}'", McpErrorCode.InvalidParams);
}
if (request.Params?.Ref?.Type == "ref/prompt")
{
// Handle completion for prompt arguments
if (!exampleCompletions.TryGetValue(request.Params.Argument.Name, out var completions))
return new CompleteResult() { Completion = new() { Values = [] } };
var values = completions.Where(value => value.StartsWith(request.Params.Argument.Value)).ToArray();
return new CompleteResult() { Completion = new() { Values = values, HasMore = false, Total = values.Length } };
}
throw new McpException($"Unknown reference type: '{request.Params?.Ref.Type}'", McpErrorCode.InvalidParams);
};
return new() { CompleteHandler = handler };
@ -541,11 +528,7 @@ internal static class Program
Messages = [new SamplingMessage()
{
Role = Role.User,
Content = new Content()
{
Type = "text",
Text = $"Resource {uri} context: {context}"
}
Content = new TextContentBlock { Text = $"Resource {uri} context: {context}" },
}],
SystemPrompt = "You are a helpful test server.",
MaxTokens = maxTokens,

View file

@ -46,11 +46,7 @@ public class Program
Messages = [new SamplingMessage()
{
Role = Role.User,
Content = new Content()
{
Type = "text",
Text = $"Resource {uri} context: {context}"
}
Content = new TextContentBlock { Text = $"Resource {uri} context: {context}" },
}],
SystemPrompt = "You are a helpful test server.",
MaxTokens = maxTokens,
@ -173,16 +169,16 @@ public class Program
{
throw new McpException("Missing required argument 'message'", McpErrorCode.InvalidParams);
}
return new CallToolResponse()
return new CallToolResult()
{
Content = [new Content() { Text = "Echo: " + message.ToString(), Type = "text" }]
Content = [new TextContentBlock { Text = $"Echo: {message}" }]
};
}
else if (request.Params.Name == "echoSessionId")
{
return new CallToolResponse()
return new CallToolResult()
{
Content = [new Content() { Text = request.Server.SessionId, Type = "text" }]
Content = [new TextContentBlock { Text = request.Server.SessionId ?? string.Empty }]
};
}
else if (request.Params.Name == "sampleLLM")
@ -196,9 +192,9 @@ public class Program
var sampleResult = await request.Server.SampleAsync(CreateRequestSamplingParams(prompt.ToString(), "sampleLLM", Convert.ToInt32(maxTokens.ToString())),
cancellationToken);
return new CallToolResponse()
return new CallToolResult()
{
Content = [new Content() { Text = $"LLM sampling result: {sampleResult.Content.Text}", Type = "text" }]
Content = [new TextContentBlock { Text = $"LLM sampling result: {(sampleResult.Content as TextContentBlock)?.Text}" }]
};
}
else
@ -308,8 +304,8 @@ public class Program
{
Name = "complex_prompt",
Description = "A prompt with arguments",
Arguments = new()
{
Arguments =
[
new PromptArgument()
{
Name = "temperature",
@ -322,7 +318,7 @@ public class Program
Description = "Output style",
Required = false
}
}
],
}
]
};
@ -339,11 +335,7 @@ public class Program
messages.Add(new PromptMessage()
{
Role = Role.User,
Content = new Content()
{
Type = "text",
Text = "This is a simple prompt without arguments."
}
Content = new TextContentBlock { Text = "This is a simple prompt without arguments." },
});
}
else if (request.Params.Name == "complex_prompt")
@ -353,27 +345,18 @@ public class Program
messages.Add(new PromptMessage()
{
Role = Role.User,
Content = new Content()
{
Type = "text",
Text = $"This is a complex prompt with arguments: temperature={temperature}, style={style}"
}
Content = new TextContentBlock { Text = $"This is a complex prompt with arguments: temperature={temperature}, style={style}" },
});
messages.Add(new PromptMessage()
{
Role = Role.Assistant,
Content = new Content()
{
Type = "text",
Text = "I understand. You've provided a complex prompt with temperature and style arguments. How would you like me to proceed?"
}
Content = new TextContentBlock { Text = "I understand. You've provided a complex prompt with temperature and style arguments. How would you like me to proceed?" },
});
messages.Add(new PromptMessage()
{
Role = Role.User,
Content = new Content()
Content = new ImageContentBlock()
{
Type = "image",
Data = MCP_TINY_IMAGE,
MimeType = "image/png"
}

View file

@ -44,15 +44,11 @@ public class McpClientExtensionsTests : ClientServerTestBase
new SamplingMessage
{
Role = Role.User,
Content = new Content { Type = "text", Text = "Hello" }
Content = new TextContentBlock { Text = "Hello" }
}
],
Temperature = temperature,
MaxTokens = maxTokens,
Meta = new RequestParamsMetadata
{
ProgressToken = new ProgressToken(),
}
};
var cancellationToken = CancellationToken.None;
@ -80,7 +76,7 @@ public class McpClientExtensionsTests : ClientServerTestBase
// Assert
Assert.NotNull(result);
Assert.Equal("Hello, World!", result.Content.Text);
Assert.Equal("Hello, World!", (result.Content as TextContentBlock)?.Text);
Assert.Equal("test-model", result.Model);
Assert.Equal(Role.Assistant, result.Role);
Assert.Equal("endTurn", result.StopReason);
@ -98,9 +94,8 @@ public class McpClientExtensionsTests : ClientServerTestBase
new SamplingMessage
{
Role = Role.User,
Content = new Content
Content = new ImageContentBlock
{
Type = "image",
MimeType = "image/png",
Data = Convert.ToBase64String(new byte[] { 1, 2, 3 })
}
@ -135,7 +130,7 @@ public class McpClientExtensionsTests : ClientServerTestBase
// Assert
Assert.NotNull(result);
Assert.Equal(expectedData, result.Content.Data);
Assert.Equal(expectedData, (result.Content as ImageContentBlock)?.Data);
Assert.Equal("test-model", result.Model);
Assert.Equal(Role.Assistant, result.Role);
Assert.Equal("endTurn", result.StopReason);
@ -162,11 +157,7 @@ public class McpClientExtensionsTests : ClientServerTestBase
new SamplingMessage
{
Role = Role.User,
Content = new Content
{
Type = "resource",
Resource = resource
},
Content = new EmbeddedResourceBlock { Resource = resource },
}
],
MaxTokens = 100
@ -284,7 +275,7 @@ public class McpClientExtensionsTests : ClientServerTestBase
JsonSerializerOptions emptyOptions = new() { TypeInfoResolver = JsonTypeInfoResolver.Combine() };
await using IMcpClient client = await CreateMcpClientForServer();
await Assert.ThrowsAsync<NotSupportedException>(async () => await client.SendRequestAsync<CallToolRequestParams, CallToolResponse>("Method4", new() { Name = "tool" }, emptyOptions, cancellationToken: TestContext.Current.CancellationToken));
await Assert.ThrowsAsync<NotSupportedException>(async () => await client.SendRequestAsync<CallToolRequestParams, CallToolResult>("Method4", new() { Name = "tool" }, emptyOptions, cancellationToken: TestContext.Current.CancellationToken));
}
[Fact]

View file

@ -70,7 +70,7 @@ public class McpClientFactoryTests
SamplingHandler = async (c, p, t) =>
new CreateMessageResult
{
Content = new Content { Text = "result" },
Content = new TextContentBlock { Text = "result" },
Model = "test-model",
Role = Role.User,
StopReason = "endTurn"

View file

@ -17,7 +17,7 @@ public partial class McpClientResourceTemplateTests : ClientServerTestBase
mcpServerBuilder.WithReadResourceHandler((request, cancellationToken) =>
new ValueTask<ReadResourceResult>(new ReadResourceResult()
{
Contents = [new TextResourceContents() { Text = request.Params?.Uri ?? string.Empty }]
Contents = [new TextResourceContents { Text = request.Params?.Uri ?? string.Empty }]
}));
}
@ -95,10 +95,10 @@ public partial class McpClientResourceTemplateTests : ClientServerTestBase
public int Level { get; set; } = 4;
[JsonPropertyName("variables")]
public Dictionary<string, JsonElement> Variables { get; set; } = [];
public IDictionary<string, JsonElement> Variables { get; set; } = new Dictionary<string, JsonElement>();
[JsonPropertyName("testcases")]
public List<List<JsonElement>> TestCases { get; set; } = [];
public IList<List<JsonElement>> TestCases { get; set; } = [];
}
[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.SnakeCaseLower)]

View file

@ -92,12 +92,12 @@ public partial class ClientIntegrationTests : LoggedTest, IClassFixture<ClientIn
// assert
Assert.NotNull(result);
Assert.False(result.IsError);
var textContent = Assert.Single(result.Content, c => c.Type == "text");
var textContent = Assert.Single(result.Content.OfType<TextContentBlock>());
Assert.Equal("Echo: Hello MCP!", textContent.Text);
}
[Fact]
public async Task CallTool_Stdio_EchoSessionId_ReturnsNull()
public async Task CallTool_Stdio_EchoSessionId_ReturnsEmpty()
{
// arrange
@ -108,8 +108,8 @@ public partial class ClientIntegrationTests : LoggedTest, IClassFixture<ClientIn
// assert
Assert.NotNull(result);
Assert.False(result.IsError);
var textContent = Assert.Single(result.Content, c => c.Type == "text");
Assert.Null(textContent.Text);
var textContent = Assert.Single(result.Content.OfType<TextContentBlock>());
Assert.Empty(textContent.Text);
}
[Theory]
@ -326,17 +326,14 @@ public partial class ClientIntegrationTests : LoggedTest, IClassFixture<ClientIn
[Theory]
[MemberData(nameof(GetClients))]
public async Task Complete_Stdio_ResourceReference(string clientId)
public async Task Complete_Stdio_ResourceTemplateReference(string clientId)
{
// arrange
// act
await using var client = await _fixture.CreateClientAsync(clientId);
var result = await client.CompleteAsync(new Reference
{
Type = "ref/resource",
Uri = "test://static/resource/1"
},
var result = await client.CompleteAsync(
new ResourceTemplateReference { Uri = "test://static/resource/1" },
"argument_name", "1",
TestContext.Current.CancellationToken
);
@ -354,11 +351,8 @@ public partial class ClientIntegrationTests : LoggedTest, IClassFixture<ClientIn
// act
await using var client = await _fixture.CreateClientAsync(clientId);
var result = await client.CompleteAsync(new Reference
{
Type = "ref/prompt",
Name = "irrelevant"
},
var result = await client.CompleteAsync(
new PromptReference { Name = "irrelevant" },
argumentName: "style", argumentValue: "fo",
TestContext.Current.CancellationToken
);
@ -387,11 +381,7 @@ public partial class ClientIntegrationTests : LoggedTest, IClassFixture<ClientIn
{
Model = "test-model",
Role = Role.Assistant,
Content = new Content
{
Type = "text",
Text = "Test response"
}
Content = new TextContentBlock { Text = "Test response" },
};
},
},
@ -410,8 +400,7 @@ public partial class ClientIntegrationTests : LoggedTest, IClassFixture<ClientIn
// assert
Assert.NotNull(result);
var textContent = Assert.Single(result.Content);
Assert.Equal("text", textContent.Type);
var textContent = Assert.Single(result.Content.OfType<TextContentBlock>());
Assert.False(string.IsNullOrEmpty(textContent.Text));
}
@ -453,7 +442,7 @@ public partial class ClientIntegrationTests : LoggedTest, IClassFixture<ClientIn
await using var client = await _fixture.CreateClientAsync(clientId);
// Verify we can send notifications without errors
await client.SendNotificationAsync(NotificationMethods.RootsUpdatedNotification, cancellationToken: TestContext.Current.CancellationToken);
await client.SendNotificationAsync(NotificationMethods.RootsListChangedNotification, cancellationToken: TestContext.Current.CancellationToken);
await client.SendNotificationAsync("test/notification", new TestNotification { Test = true }, cancellationToken: TestContext.Current.CancellationToken, serializerOptions: JsonContext3.Default.Options);
// assert
@ -556,9 +545,9 @@ public partial class ClientIntegrationTests : LoggedTest, IClassFixture<ClientIn
Assert.NotNull(result);
Assert.NotEmpty(result.Content);
Assert.Equal("text", result.Content[0].Type);
Assert.Contains("LLM sampling result:", result.Content[0].Text);
Assert.Contains("Eiffel", result.Content[0].Text);
var content = Assert.IsType<TextContentBlock>(result.Content[0]);
Assert.Contains("LLM sampling result:", content.Text);
Assert.Contains("Eiffel", content.Text);
}
[Theory]

View file

@ -34,7 +34,7 @@ public class McpServerBuilderExtensionsHandlerTests
[Fact]
public void WithCallToolHandler_Sets_Handler()
{
Func<RequestContext<CallToolRequestParams>, CancellationToken, ValueTask<CallToolResponse>> handler = async (context, token) => new CallToolResponse();
Func<RequestContext<CallToolRequestParams>, CancellationToken, ValueTask<CallToolResult>> handler = async (context, token) => new CallToolResult();
_builder.Object.WithCallToolHandler(handler);

View file

@ -71,7 +71,7 @@ public partial class McpServerBuilderExtensionsPromptsTests : ClientServerTestBa
case "FinalCustomPrompt":
return new GetPromptResult()
{
Messages = [new() { Role = Role.User, Content = new() { Text = $"hello from {request.Params.Name}", Type = "text" } }],
Messages = [new() { Role = Role.User, Content = new TextContentBlock { Text = $"hello from {request.Params.Name}" } }],
};
default:
@ -162,6 +162,20 @@ public partial class McpServerBuilderExtensionsPromptsTests : ClientServerTestBa
Assert.DoesNotContain(prompts, t => t.Name == "NewPrompt");
}
[Fact]
public async Task TitleAttributeProperty_PropagatedToTitle()
{
await using IMcpClient client = await CreateMcpClientForServer();
var prompts = await client.ListPromptsAsync(cancellationToken: TestContext.Current.CancellationToken);
Assert.NotNull(prompts);
Assert.NotEmpty(prompts);
McpClientPrompt prompt = prompts.First(t => t.Name == nameof(SimplePrompts.ReturnsString));
Assert.Equal("This is a title", prompt.Title);
}
[Fact]
public async Task Throws_When_Prompt_Fails()
{
@ -263,7 +277,7 @@ public partial class McpServerBuilderExtensionsPromptsTests : ClientServerTestBa
public static ChatMessage[] ThrowsException([Description("The first parameter")] string message) =>
throw new FormatException("uh oh");
[McpServerPrompt, Description("Returns chat messages")]
[McpServerPrompt(Title = "This is a title"), Description("Returns chat messages")]
public string ReturnsString([Description("The first parameter")] string message) =>
$"The prompt is: {message}. The id is {id}.";
}
@ -273,10 +287,10 @@ public partial class McpServerBuilderExtensionsPromptsTests : ClientServerTestBa
{
[McpServerPrompt]
public static PromptMessage AnotherPrompt(ObjectWithId id) =>
new PromptMessage
new()
{
Role = Role.User,
Content = new() { Text = "hello", Type = "text" },
Content = new TextContentBlock { Text = "hello" },
};
}

View file

@ -101,7 +101,7 @@ public partial class McpServerBuilderExtensionsResourcesTests : ClientServerTest
case "test://ResourceTemplate2":
return new ReadResourceResult()
{
Contents = [new TextResourceContents() { Text = request.Params?.Uri ?? "(null)" }]
Contents = [new TextResourceContents { Text = request.Params?.Uri ?? "(null)" }]
};
}
@ -196,6 +196,24 @@ public partial class McpServerBuilderExtensionsResourcesTests : ClientServerTest
Assert.DoesNotContain(resources, t => t.Name == "NewResource");
}
[Fact]
public async Task TitleAttributeProperty_PropagatedToTitle()
{
await using IMcpClient client = await CreateMcpClientForServer();
var resources = await client.ListResourcesAsync(cancellationToken: TestContext.Current.CancellationToken);
Assert.NotNull(resources);
Assert.NotEmpty(resources);
McpClientResource resource = resources.First(t => t.Name == nameof(SimpleResources.SomeNeatDirectResource));
Assert.Equal("This is a title", resource.Title);
var resourceTemplates = await client.ListResourceTemplatesAsync(cancellationToken: TestContext.Current.CancellationToken);
Assert.NotNull(resourceTemplates);
Assert.NotEmpty(resourceTemplates);
McpClientResourceTemplate resourceTemplate = resourceTemplates.First(t => t.Name == nameof(SimpleResources.SomeNeatTemplatedResource));
Assert.Equal("This is another title", resourceTemplate.Title);
}
[Fact]
public async Task Throws_When_Resource_Fails()
{
@ -273,10 +291,10 @@ public partial class McpServerBuilderExtensionsResourcesTests : ClientServerTest
[McpServerResourceType]
public sealed class SimpleResources
{
[McpServerResource, Description("Some neat direct resource")]
[McpServerResource(Title = "This is a title"), Description("Some neat direct resource")]
public static string SomeNeatDirectResource() => "This is a neat resource";
[McpServerResource, Description("Some neat resource with parameters")]
[McpServerResource(Title = "This is another title"), Description("Some neat resource with parameters")]
public static string SomeNeatTemplatedResource(string name) => $"This is a neat resource with parameters: {name}";
[McpServerResource]

View file

@ -95,9 +95,9 @@ public partial class McpServerBuilderExtensionsToolsTests : ClientServerTestBase
case "FirstCustomTool":
case "SecondCustomTool":
case "FinalCustomTool":
return new CallToolResponse()
return new CallToolResult()
{
Content = [new Content() { Text = $"{request.Params.Name}Result", Type = "text" }],
Content = [new TextContentBlock { Text = $"{request.Params.Name}Result" }],
};
default:
@ -239,8 +239,9 @@ public partial class McpServerBuilderExtensionsToolsTests : ClientServerTestBase
Assert.NotNull(result.Content);
Assert.NotEmpty(result.Content);
Assert.Equal("hello Peter", result.Content[0].Text);
Assert.Equal("text", result.Content[0].Type);
var tc = Assert.IsType<TextContentBlock>(result.Content[0]);
Assert.Equal("hello Peter", tc.Text);
Assert.Equal("text", tc.Type);
}
[Fact]
@ -255,8 +256,8 @@ public partial class McpServerBuilderExtensionsToolsTests : ClientServerTestBase
Assert.NotNull(result.Content);
Assert.NotEmpty(result.Content);
Assert.Equal("hello Peter", result.Content[0].Text);
Assert.Equal("hello2 Peter", result.Content[1].Text);
Assert.Equal("hello Peter", (result.Content[0] as TextContentBlock)?.Text);
Assert.Equal("hello2 Peter", (result.Content[1] as TextContentBlock)?.Text);
result = await client.CallToolAsync(
"SecondCustomTool",
@ -264,7 +265,7 @@ public partial class McpServerBuilderExtensionsToolsTests : ClientServerTestBase
Assert.NotNull(result);
Assert.NotNull(result.Content);
Assert.NotEmpty(result.Content);
Assert.Equal("SecondCustomToolResult", result.Content[0].Text);
Assert.Equal("SecondCustomToolResult", (result.Content[0] as TextContentBlock)?.Text);
}
[Fact]
@ -294,8 +295,8 @@ public partial class McpServerBuilderExtensionsToolsTests : ClientServerTestBase
Assert.NotNull(result.Content);
Assert.NotEmpty(result.Content);
Assert.Equal("""{"SomeProp":false}""", Regex.Replace(result.Content[0].Text ?? string.Empty, "\\s+", ""));
Assert.Equal("text", result.Content[0].Type);
Assert.Equal("""{"SomeProp":false}""", Regex.Replace((result.Content[0] as TextContentBlock)?.Text ?? string.Empty, "\\s+", ""));
Assert.Equal("text", (result.Content[0] as TextContentBlock)?.Type);
}
[Fact]
@ -310,8 +311,7 @@ public partial class McpServerBuilderExtensionsToolsTests : ClientServerTestBase
Assert.NotNull(result.Content);
Assert.NotEmpty(result.Content);
Assert.Equal("5", result.Content[0].Text);
Assert.Equal("text", result.Content[0].Type);
Assert.Equal("5", (result.Content[0] as TextContentBlock)?.Text);
}
[Fact]
@ -328,8 +328,7 @@ public partial class McpServerBuilderExtensionsToolsTests : ClientServerTestBase
Assert.NotNull(result.Content);
Assert.NotEmpty(result.Content);
Assert.Equal("Peter", result.Content[0].Text);
Assert.Equal("text", result.Content[0].Type);
Assert.Equal("Peter", (result.Content[0] as TextContentBlock)?.Text);
}
[Fact]
@ -348,7 +347,7 @@ public partial class McpServerBuilderExtensionsToolsTests : ClientServerTestBase
Assert.NotNull(result.Content);
Assert.NotEmpty(result.Content);
parts[i] = result.Content[0].Text?.Split(':') ?? [];
parts[i] = (result.Content[0] as TextContentBlock)?.Text?.Split(':') ?? [];
Assert.Equal(2, parts[i].Length);
}
@ -373,7 +372,7 @@ public partial class McpServerBuilderExtensionsToolsTests : ClientServerTestBase
Assert.True(result.IsError);
Assert.NotNull(result.Content);
Assert.NotEmpty(result.Content);
Assert.Contains("An error occurred", result.Content[0].Text);
Assert.Contains("An error occurred", (result.Content[0] as TextContentBlock)?.Text);
}
[Fact]
@ -560,6 +559,7 @@ public partial class McpServerBuilderExtensionsToolsTests : ClientServerTestBase
var annotations = tool.ProtocolTool.Annotations;
Assert.NotNull(annotations);
Assert.Equal("Return An Integer", annotations.Title);
Assert.Equal("Return An Integer", tool.ProtocolTool.Title);
Assert.False(annotations.DestructiveHint);
Assert.True(annotations.IdempotentHint);
Assert.False(annotations.OpenWorldHint);
@ -582,6 +582,22 @@ public partial class McpServerBuilderExtensionsToolsTests : ClientServerTestBase
Assert.Null(annotations.ReadOnlyHint);
}
[Fact]
public async Task TitleAttributeProperty_PropagatedToTitle()
{
await using IMcpClient client = await CreateMcpClientForServer();
var tools = await client.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken);
Assert.NotNull(tools);
Assert.NotEmpty(tools);
McpClientTool tool = tools.First(t => t.Name == nameof(EchoTool.EchoComplex));
Assert.Equal("This is a title", tool.Title);
Assert.Equal("This is a title", tool.ProtocolTool.Title);
Assert.Equal("This is a title", tool.ProtocolTool.Annotations?.Title);
}
[Fact]
public async Task HandlesIProgressParameter()
{
@ -596,10 +612,10 @@ public partial class McpServerBuilderExtensionsToolsTests : ClientServerTestBase
TaskCompletionSource tcs = new(TaskCreationOptions.RunContinuationsAsynchronously);
int remainingNotifications = 10;
ConcurrentQueue<ProgressNotification> notifications = new();
ConcurrentQueue<ProgressNotificationParams> notifications = new();
await using (client.RegisterNotificationHandler(NotificationMethods.ProgressNotification, (notification, cancellationToken) =>
{
if (JsonSerializer.Deserialize<ProgressNotification>(notification.Params, McpJsonUtilities.DefaultOptions) is { } pn &&
if (JsonSerializer.Deserialize<ProgressNotificationParams>(notification.Params, McpJsonUtilities.DefaultOptions) is { } pn &&
pn.ProgressToken == new ProgressToken("abc123"))
{
notifications.Enqueue(pn);
@ -612,12 +628,12 @@ public partial class McpServerBuilderExtensionsToolsTests : ClientServerTestBase
return default;
}))
{
var result = await client.SendRequestAsync<CallToolRequestParams, CallToolResponse>(
var result = await client.SendRequestAsync<CallToolRequestParams, CallToolResult>(
RequestMethods.ToolsCall,
new CallToolRequestParams
{
Name = progressTool.ProtocolTool.Name,
Meta = new() { ProgressToken = new("abc123") },
ProgressToken = new("abc123"),
},
cancellationToken: TestContext.Current.CancellationToken);
@ -625,7 +641,7 @@ public partial class McpServerBuilderExtensionsToolsTests : ClientServerTestBase
Assert.Contains("done", JsonSerializer.Serialize(result, McpJsonUtilities.DefaultOptions));
}
ProgressNotification[] array = notifications.OrderBy(n => n.Progress.Progress).ToArray();
ProgressNotificationParams[] array = notifications.OrderBy(n => n.Progress.Progress).ToArray();
Assert.Equal(10, array.Length);
for (int i = 0; i < array.Length; i++)
{
@ -647,7 +663,7 @@ public partial class McpServerBuilderExtensionsToolsTests : ClientServerTestBase
McpClientTool cancelableTool = tools.First(t => t.Name == nameof(EchoTool.InfiniteCancelableOperation));
var requestId = new RequestId(Guid.NewGuid().ToString());
var invokeTask = client.SendRequestAsync<CallToolRequestParams, CallToolResponse>(
var invokeTask = client.SendRequestAsync<CallToolRequestParams, CallToolResult>(
RequestMethods.ToolsCall,
new CallToolRequestParams { Name = cancelableTool.ProtocolTool.Name },
requestId: requestId,
@ -655,7 +671,7 @@ public partial class McpServerBuilderExtensionsToolsTests : ClientServerTestBase
await client.SendNotificationAsync(
NotificationMethods.CancelledNotification,
parameters: new CancelledNotification()
parameters: new CancelledNotificationParams()
{
RequestId = requestId,
},
@ -723,7 +739,7 @@ public partial class McpServerBuilderExtensionsToolsTests : ClientServerTestBase
return cancellationToken.GetHashCode();
}
[McpServerTool]
[McpServerTool(Title = "This is a title")]
public static string EchoComplex(ComplexObject complex)
{
return complex.Name!;

View file

@ -2,7 +2,6 @@
using Microsoft.Extensions.Hosting;
using ModelContextProtocol.Protocol;
using ModelContextProtocol.Server;
using Moq;
using System.IO.Pipelines;
namespace ModelContextProtocol.Tests.Configuration;

View file

@ -16,7 +16,7 @@ public partial class McpServerScopedTests : ClientServerTestBase
protected override void ConfigureServices(ServiceCollection services, IMcpServerBuilder mcpServerBuilder)
{
mcpServerBuilder.WithTools<EchoTool>(serializerOptions: McpServerScopedTestsJsonContext.Default.Options);
services.AddScoped(_ => new ComplexObject() { Name = "Scoped" });
services.AddScoped(_ => new ComplexObject { Name = "Scoped" });
}
[Fact]

View file

@ -83,11 +83,7 @@ public class DockerEverythingServerTests(ITestOutputHelper testOutputHelper) : L
{
Model = "test-model",
Role = Role.Assistant,
Content = new Content
{
Type = "text",
Text = "Test response"
}
Content = new TextContentBlock { Text = "Test response" },
};
},
},
@ -109,7 +105,7 @@ public class DockerEverythingServerTests(ITestOutputHelper testOutputHelper) : L
// assert
Assert.NotNull(result);
var textContent = Assert.Single(result.Content);
var textContent = Assert.Single(result.Content.OfType<TextContentBlock>());
Assert.Equal("text", textContent.Type);
Assert.False(string.IsNullOrEmpty(textContent.Text));
}

Some files were not shown because too many files have changed in this diff Show more