Add tools support to sampling (#976)

This commit is contained in:
Stephen Toub 2025-11-20 09:52:23 -05:00 • committed by GitHub
commit 15139cc569
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 1623 additions and 425 deletions

View file

@ -1,4 +1,3 @@
using Microsoft.Extensions.AI;
using ModelContextProtocol.Protocol;
using System.Text.Json;
@ -125,4 +124,105 @@ public class ContentBlockTests
var textBlock = Assert.IsType<TextContentBlock>(contentBlock);
Assert.Contains("Sample text", textBlock.Text);
}
[Fact]
public void ToolResultContentBlock_WithError_SerializationRoundtrips()
{
ToolResultContentBlock toolResult = new()
{
ToolUseId = "call_123",
Content = [new TextContentBlock { Text = "Error: City not found" }],
IsError = true
};
var json = JsonSerializer.Serialize<ContentBlock>(toolResult, McpJsonUtilities.DefaultOptions);
var deserialized = JsonSerializer.Deserialize<ContentBlock>(json, McpJsonUtilities.DefaultOptions);
var result = Assert.IsType<ToolResultContentBlock>(deserialized);
Assert.Equal("call_123", result.ToolUseId);
Assert.True(result.IsError);
Assert.Single(result.Content);
var textBlock = Assert.IsType<TextContentBlock>(result.Content[0]);
Assert.Equal("Error: City not found", textBlock.Text);
}
[Fact]
public void ToolResultContentBlock_WithStructuredContent_SerializationRoundtrips()
{
ToolResultContentBlock toolResult = new()
{
ToolUseId = "call_123",
Content =
[
new TextContentBlock { Text = "Result data" }
],
StructuredContent = JsonElement.Parse("""{"temperature":18,"condition":"cloudy"}"""),
IsError = false
};
var json = JsonSerializer.Serialize<ContentBlock>(toolResult, McpJsonUtilities.DefaultOptions);
var deserialized = JsonSerializer.Deserialize<ContentBlock>(json, McpJsonUtilities.DefaultOptions);
var result = Assert.IsType<ToolResultContentBlock>(deserialized);
Assert.Equal("call_123", result.ToolUseId);
Assert.Single(result.Content);
var textBlock = Assert.IsType<TextContentBlock>(result.Content[0]);
Assert.Equal("Result data", textBlock.Text);
Assert.NotNull(result.StructuredContent);
Assert.Equal(18, result.StructuredContent.Value.GetProperty("temperature").GetInt32());
Assert.Equal("cloudy", result.StructuredContent.Value.GetProperty("condition").GetString());
Assert.False(result.IsError);
}
[Fact]
public void ToolResultContentBlock_SerializationRoundTrip()
{
ToolResultContentBlock toolResult = new()
{
ToolUseId = "call_123",
Content =
[
new TextContentBlock { Text = "Result data" },
new ImageContentBlock { Data = "base64data", MimeType = "image/png" }
],
StructuredContent = JsonElement.Parse("""{"temperature":18,"condition":"cloudy"}"""),
IsError = false
};
var json = JsonSerializer.Serialize<ContentBlock>(toolResult, McpJsonUtilities.DefaultOptions);
var deserialized = JsonSerializer.Deserialize<ContentBlock>(json, McpJsonUtilities.DefaultOptions);
var result = Assert.IsType<ToolResultContentBlock>(deserialized);
Assert.Equal("call_123", result.ToolUseId);
Assert.Equal(2, result.Content.Count);
var textBlock = Assert.IsType<TextContentBlock>(result.Content[0]);
Assert.Equal("Result data", textBlock.Text);
var imageBlock = Assert.IsType<ImageContentBlock>(result.Content[1]);
Assert.Equal("base64data", imageBlock.Data);
Assert.Equal("image/png", imageBlock.MimeType);
Assert.NotNull(result.StructuredContent);
Assert.Equal(18, result.StructuredContent.Value.GetProperty("temperature").GetInt32());
Assert.Equal("cloudy", result.StructuredContent.Value.GetProperty("condition").GetString());
Assert.False(result.IsError);
}
[Fact]
public void ToolUseContentBlock_SerializationRoundTrip()
{
ToolUseContentBlock toolUse = new()
{
Id = "call_abc123",
Name = "get_weather",
Input = JsonElement.Parse("""{"city":"Paris","units":"metric"}""")
};
var json = JsonSerializer.Serialize<ContentBlock>(toolUse, McpJsonUtilities.DefaultOptions);
var deserialized = JsonSerializer.Deserialize<ContentBlock>(json, McpJsonUtilities.DefaultOptions);
var result = Assert.IsType<ToolUseContentBlock>(deserialized);
Assert.Equal("call_abc123", result.Id);
Assert.Equal("get_weather", result.Name);
Assert.Equal("Paris", result.Input.GetProperty("city").GetString());
Assert.Equal("metric", result.Input.GetProperty("units").GetString());
}
}

View file

@ -0,0 +1,174 @@
using ModelContextProtocol.Protocol;
using System.Text.Json;
namespace ModelContextProtocol.Tests.Protocol;
public class CreateMessageRequestParamsTests
{
[Fact]
public void WithTools_SerializationRoundtrips()
{
CreateMessageRequestParams requestParams = new()
{
MaxTokens = 1000,
Messages =
[
new SamplingMessage
{
Role = Role.User,
Content = [new TextContentBlock { Text = "What's the weather in Paris?" }]
}
],
Tools =
[
new Tool
{
Name = "get_weather",
Description = "Get weather for a city",
InputSchema = JsonElement.Parse("""
{
"type": "object",
"properties": {
"city": { "type": "string" }
},
"required": ["city"]
}
""")
}
],
ToolChoice = new ToolChoice { Mode = "auto" }
};
var json = JsonSerializer.Serialize(requestParams, McpJsonUtilities.DefaultOptions);
var deserialized = JsonSerializer.Deserialize<CreateMessageRequestParams>(json, McpJsonUtilities.DefaultOptions);
Assert.NotNull(deserialized);
Assert.Equal(1000, deserialized.MaxTokens);
Assert.NotNull(deserialized.Messages);
Assert.Single(deserialized.Messages);
Assert.Equal(Role.User, deserialized.Messages[0].Role);
Assert.Single(deserialized.Messages[0].Content);
var textContent = Assert.IsType<TextContentBlock>(deserialized.Messages[0].Content[0]);
Assert.Equal("What's the weather in Paris?", textContent.Text);
Assert.NotNull(deserialized.Tools);
Assert.Single(deserialized.Tools);
Assert.Equal("get_weather", deserialized.Tools[0].Name);
Assert.Equal("Get weather for a city", deserialized.Tools[0].Description);
Assert.Equal("object", deserialized.Tools[0].InputSchema.GetProperty("type").GetString());
Assert.True(deserialized.Tools[0].InputSchema.GetProperty("properties").TryGetProperty("city", out var cityProp));
Assert.Equal("string", cityProp.GetProperty("type").GetString());
Assert.Single(deserialized.Tools[0].InputSchema.GetProperty("required").EnumerateArray());
Assert.Equal("city", deserialized.Tools[0].InputSchema.GetProperty("required")[0].GetString());
Assert.NotNull(deserialized.ToolChoice);
Assert.Equal("auto", deserialized.ToolChoice.Mode);
}
[Fact]
public void WithToolChoiceRequired_SerializationRoundtrips()
{
CreateMessageRequestParams requestParams = new()
{
MaxTokens = 1000,
Messages =
[
new SamplingMessage
{
Role = Role.User,
Content = [new TextContentBlock { Text = "What's the weather?" }]
}
],
Tools =
[
new Tool
{
Name = "get_weather",
Description = "Get weather for a city",
InputSchema = JsonElement.Parse("""
{
"type": "object",
"properties": { "city": { "type": "string" } },
"required": ["city"]
}
""")
}
],
ToolChoice = new ToolChoice { Mode = "required" }
};
var json = JsonSerializer.Serialize(requestParams, McpJsonUtilities.DefaultOptions);
var deserialized = JsonSerializer.Deserialize<CreateMessageRequestParams>(json, McpJsonUtilities.DefaultOptions);
Assert.NotNull(deserialized);
Assert.Equal(1000, deserialized.MaxTokens);
Assert.NotNull(deserialized.Messages);
Assert.Single(deserialized.Messages);
Assert.Equal(Role.User, deserialized.Messages[0].Role);
Assert.Single(deserialized.Messages[0].Content);
var textContent = Assert.IsType<TextContentBlock>(deserialized.Messages[0].Content[0]);
Assert.Equal("What's the weather?", textContent.Text);
Assert.NotNull(deserialized.Tools);
Assert.Single(deserialized.Tools);
Assert.Equal("get_weather", deserialized.Tools[0].Name);
Assert.Equal("Get weather for a city", deserialized.Tools[0].Description);
Assert.Equal("object", deserialized.Tools[0].InputSchema.GetProperty("type").GetString());
Assert.NotNull(deserialized.ToolChoice);
Assert.Equal("required", deserialized.ToolChoice.Mode);
}
[Fact]
public void WithToolChoiceNone_SerializationRoundtrips()
{
CreateMessageRequestParams requestParams = new()
{
MaxTokens = 1000,
Messages =
[
new SamplingMessage
{
Role = Role.User,
Content = [new TextContentBlock { Text = "What's the weather in Paris?" }]
}
],
Tools =
[
new Tool
{
Name = "get_weather",
Description = "Get weather for a city",
InputSchema = JsonElement.Parse("""
{
"type": "object",
"properties": { "city": { "type": "string" } },
"required": ["city"]
}
""")
}
],
ToolChoice = new ToolChoice { Mode = "none" }
};
var json = JsonSerializer.Serialize(requestParams, McpJsonUtilities.DefaultOptions);
var deserialized = JsonSerializer.Deserialize<CreateMessageRequestParams>(json, McpJsonUtilities.DefaultOptions);
Assert.NotNull(deserialized);
Assert.Equal(1000, deserialized.MaxTokens);
Assert.NotNull(deserialized.Messages);
Assert.Single(deserialized.Messages);
Assert.Equal(Role.User, deserialized.Messages[0].Role);
Assert.Single(deserialized.Messages[0].Content);
var textContent = Assert.IsType<TextContentBlock>(deserialized.Messages[0].Content[0]);
Assert.Equal("What's the weather in Paris?", textContent.Text);
Assert.NotNull(deserialized.Tools);
Assert.Single(deserialized.Tools);
Assert.Equal("get_weather", deserialized.Tools[0].Name);
Assert.Equal("Get weather for a city", deserialized.Tools[0].Description);
Assert.Equal("object", deserialized.Tools[0].InputSchema.GetProperty("type").GetString());
Assert.NotNull(deserialized.ToolChoice);
Assert.Equal("none", deserialized.ToolChoice.Mode);
}
}

View file

@ -0,0 +1,247 @@
using ModelContextProtocol.Protocol;
using System.Text.Json;
using System.Text.Json.Nodes;
namespace ModelContextProtocol.Tests.Protocol;
public class CreateMessageResultTests
{
[Fact]
public void CreateMessageResult_WithSingleContent_Serializes()
{
CreateMessageResult result = new()
{
Role = Role.Assistant,
Model = "test-model",
Content = [new TextContentBlock { Text = "Hello" }],
StopReason = "endTurn"
};
var json = JsonSerializer.Serialize(result, McpJsonUtilities.DefaultOptions);
var deserialized = JsonSerializer.Deserialize<CreateMessageResult>(json, McpJsonUtilities.DefaultOptions);
Assert.NotNull(deserialized);
Assert.Single(deserialized.Content);
Assert.IsType<TextContentBlock>(deserialized.Content[0]);
}
[Fact]
public void CreateMessageResult_WithMultipleToolUses_Serializes()
{
CreateMessageResult result = new()
{
Role = Role.Assistant,
Model = "test-model",
Content =
[
new ToolUseContentBlock
{
Id = "call_1",
Name = "tool1",
Input = JsonElement.Parse("""{}""")
},
new ToolUseContentBlock
{
Id = "call_2",
Name = "tool2",
Input = JsonElement.Parse("""{}""")
}
],
StopReason = "toolUse"
};
var json = JsonSerializer.Serialize(result, McpJsonUtilities.DefaultOptions);
var deserialized = JsonSerializer.Deserialize<CreateMessageResult>(json, McpJsonUtilities.DefaultOptions);
Assert.NotNull(deserialized);
Assert.Equal(2, deserialized.Content.Count);
Assert.All(deserialized.Content, c => Assert.IsType<ToolUseContentBlock>(c));
Assert.Equal("call_1", ((ToolUseContentBlock)deserialized.Content[0]).Id);
Assert.Equal("call_2", ((ToolUseContentBlock)deserialized.Content[1]).Id);
}
[Fact]
public void CreateMessageResult_WithMixedContent_Serializes()
{
CreateMessageResult result = new()
{
Role = Role.Assistant,
Model = "test-model",
Content =
[
new TextContentBlock { Text = "Let me check that." },
new ToolUseContentBlock
{
Id = "call_1",
Name = "tool1",
Input = JsonElement.Parse("""{}""")
}
],
StopReason = "toolUse"
};
var json = JsonSerializer.Serialize(result, McpJsonUtilities.DefaultOptions);
var deserialized = JsonSerializer.Deserialize<CreateMessageResult>(json, McpJsonUtilities.DefaultOptions);
Assert.NotNull(deserialized);
Assert.Equal(2, deserialized.Content.Count);
Assert.IsType<TextContentBlock>(deserialized.Content[0]);
Assert.IsType<ToolUseContentBlock>(deserialized.Content[1]);
}
[Fact]
public void CreateMessageResult_EmptyContent_AllowedButUnusual()
{
CreateMessageResult result = new()
{
Role = Role.Assistant,
Model = "test-model",
Content = [],
StopReason = "endTurn"
};
var json = JsonSerializer.Serialize(result, McpJsonUtilities.DefaultOptions);
var deserialized = JsonSerializer.Deserialize<CreateMessageResult>(json, McpJsonUtilities.DefaultOptions);
Assert.NotNull(deserialized);
Assert.Empty(deserialized.Content);
}
[Fact]
public void CreateMessageResult_WithImageContent_Serializes()
{
CreateMessageResult result = new()
{
Role = Role.Assistant,
Model = "test-model",
Content =
[
new ImageContentBlock
{
Data = Convert.ToBase64String([1, 2, 3, 4, 5]),
MimeType = "image/png"
}
],
StopReason = "endTurn"
};
var json = JsonSerializer.Serialize(result, McpJsonUtilities.DefaultOptions);
var deserialized = JsonSerializer.Deserialize<CreateMessageResult>(json, McpJsonUtilities.DefaultOptions);
Assert.NotNull(deserialized);
Assert.Single(deserialized.Content);
var imageBlock = Assert.IsType<ImageContentBlock>(deserialized.Content[0]);
Assert.Equal("image/png", imageBlock.MimeType);
}
[Fact]
public void CreateMessageResult_RoundTripWithAllFields()
{
CreateMessageResult original = new()
{
Role = Role.Assistant,
Model = "claude-3-sonnet",
Content =
[
new TextContentBlock { Text = "I'll help you with that." },
new ToolUseContentBlock
{
Id = "call_xyz",
Name = "calculator",
Input = JsonElement.Parse("""{"operation":"add","a":5,"b":3}""")
}
],
StopReason = "toolUse",
Meta = (JsonObject)JsonNode.Parse("""{"custom":"metadata"}""")!
};
var json = JsonSerializer.Serialize(original, McpJsonUtilities.DefaultOptions);
var deserialized = JsonSerializer.Deserialize<CreateMessageResult>(json, McpJsonUtilities.DefaultOptions);
Assert.NotNull(deserialized);
Assert.Equal(Role.Assistant, deserialized.Role);
Assert.Equal("claude-3-sonnet", deserialized.Model);
Assert.Equal(2, deserialized.Content.Count);
Assert.Equal("toolUse", deserialized.StopReason);
Assert.NotNull(deserialized.Meta);
}
[Fact]
public void CreateMessageResult_WithToolUse_SerializationRoundtrips()
{
CreateMessageResult result = new()
{
Role = Role.Assistant,
Model = "test-model",
Content =
[
new ToolUseContentBlock
{
Id = "call_123",
Name = "get_weather",
Input = JsonElement.Parse("""{"city":"Paris"}""")
}
],
StopReason = "toolUse"
};
var json = JsonSerializer.Serialize(result, McpJsonUtilities.DefaultOptions);
var deserialized = JsonSerializer.Deserialize<CreateMessageResult>(json, McpJsonUtilities.DefaultOptions);
Assert.NotNull(deserialized);
Assert.Equal(Role.Assistant, deserialized.Role);
Assert.Equal("test-model", deserialized.Model);
Assert.Equal("toolUse", deserialized.StopReason);
Assert.Single(deserialized.Content);
var toolUse = Assert.IsType<ToolUseContentBlock>(deserialized.Content[0]);
Assert.Equal("call_123", toolUse.Id);
Assert.Equal("get_weather", toolUse.Name);
Assert.Equal("Paris", toolUse.Input.GetProperty("city").GetString());
}
[Fact]
public void CreateMessageResult_WithParallelToolUses_SerializationRoundtrips()
{
CreateMessageResult result = new()
{
Role = Role.Assistant,
Model = "test-model",
Content =
[
new ToolUseContentBlock
{
Id = "call_abc123",
Name = "get_weather",
Input = JsonElement.Parse("""{"city":"Paris"}""")
},
new ToolUseContentBlock
{
Id = "call_def456",
Name = "get_weather",
Input = JsonElement.Parse("""{"city":"London"}""")
}
],
StopReason = "toolUse"
};
var json = JsonSerializer.Serialize(result, McpJsonUtilities.DefaultOptions);
var deserialized = JsonSerializer.Deserialize<CreateMessageResult>(json, McpJsonUtilities.DefaultOptions);
Assert.NotNull(deserialized);
Assert.Equal(Role.Assistant, deserialized.Role);
Assert.Equal("test-model", deserialized.Model);
Assert.Equal("toolUse", deserialized.StopReason);
Assert.Equal(2, deserialized.Content.Count);
var toolUse1 = Assert.IsType<ToolUseContentBlock>(deserialized.Content[0]);
Assert.Equal("call_abc123", toolUse1.Id);
Assert.Equal("get_weather", toolUse1.Name);
Assert.Equal("Paris", toolUse1.Input.GetProperty("city").GetString());
var toolUse2 = Assert.IsType<ToolUseContentBlock>(deserialized.Content[1]);
Assert.Equal("call_def456", toolUse2.Id);
Assert.Equal("get_weather", toolUse2.Name);
Assert.Equal("London", toolUse2.Input.GetProperty("city").GetString());
}
}

View file

@ -0,0 +1,111 @@
using ModelContextProtocol.Protocol;
using System.Text.Json;
namespace ModelContextProtocol.Tests.Protocol;
public class SamplingMessageTests
{
[Fact]
public void WithToolResults_SerializationRoundtrips()
{
SamplingMessage message = new()
{
Role = Role.User,
Content =
[
new ToolResultContentBlock
{
ToolUseId = "call_123",
Content =
[
new TextContentBlock { Text = "Weather in Paris: 18°C, partly cloudy" }
]
}
]
};
var json = JsonSerializer.Serialize(message, McpJsonUtilities.DefaultOptions);
var deserialized = JsonSerializer.Deserialize<SamplingMessage>(json, McpJsonUtilities.DefaultOptions);
Assert.NotNull(deserialized);
Assert.Equal(Role.User, deserialized.Role);
Assert.Single(deserialized.Content);
var toolResult = Assert.IsType<ToolResultContentBlock>(deserialized.Content[0]);
Assert.Equal("call_123", toolResult.ToolUseId);
Assert.Single(toolResult.Content);
var textBlock = Assert.IsType<TextContentBlock>(toolResult.Content[0]);
Assert.Equal("Weather in Paris: 18°C, partly cloudy", textBlock.Text);
}
[Fact]
public void WithMultipleToolResults_SerializationRoundtrips()
{
SamplingMessage message = new()
{
Role = Role.User,
Content =
[
new ToolResultContentBlock
{
ToolUseId = "call_abc123",
Content = [new TextContentBlock { Text = "Weather in Paris: 18°C, partly cloudy" }]
},
new ToolResultContentBlock
{
ToolUseId = "call_def456",
Content = [new TextContentBlock { Text = "Weather in London: 15°C, rainy" }]
}
]
};
var json = JsonSerializer.Serialize(message, McpJsonUtilities.DefaultOptions);
var deserialized = JsonSerializer.Deserialize<SamplingMessage>(json, McpJsonUtilities.DefaultOptions);
Assert.NotNull(deserialized);
Assert.Equal(Role.User, deserialized.Role);
Assert.Equal(2, deserialized.Content.Count);
var toolResult1 = Assert.IsType<ToolResultContentBlock>(deserialized.Content[0]);
Assert.Equal("call_abc123", toolResult1.ToolUseId);
Assert.Single(toolResult1.Content);
var textBlock1 = Assert.IsType<TextContentBlock>(toolResult1.Content[0]);
Assert.Equal("Weather in Paris: 18°C, partly cloudy", textBlock1.Text);
var toolResult2 = Assert.IsType<ToolResultContentBlock>(deserialized.Content[1]);
Assert.Equal("call_def456", toolResult2.ToolUseId);
Assert.Single(toolResult2.Content);
var textBlock2 = Assert.IsType<TextContentBlock>(toolResult2.Content[0]);
Assert.Equal("Weather in London: 15°C, rainy", textBlock2.Text);
}
[Fact]
public void WithToolResultOnly_SerializationRoundtrips()
{
SamplingMessage message = new()
{
Role = Role.User,
Content =
[
new ToolResultContentBlock
{
ToolUseId = "call_123",
Content = [new TextContentBlock { Text = "Result" }]
}
]
};
var json = JsonSerializer.Serialize(message, McpJsonUtilities.DefaultOptions);
var deserialized = JsonSerializer.Deserialize<SamplingMessage>(json, McpJsonUtilities.DefaultOptions);
Assert.NotNull(deserialized);
Assert.Equal(Role.User, deserialized.Role);
Assert.Single(deserialized.Content);
var toolResult = Assert.IsType<ToolResultContentBlock>(deserialized.Content[0]);
Assert.Equal("call_123", toolResult.ToolUseId);
Assert.Single(toolResult.Content);
var textBlock = Assert.IsType<TextContentBlock>(toolResult.Content[0]);
Assert.Equal("Result", textBlock.Text);
}
}

View file

@ -0,0 +1,30 @@
using ModelContextProtocol.Protocol;
using System.Text.Json;
namespace ModelContextProtocol.Tests.Protocol;
public class ToolChoiceTests
{
[Fact]
public void DefaultModeIsNull()
{
Assert.Null(new ToolChoice().Mode);
}
[Theory]
[InlineData(null)]
[InlineData("none")]
[InlineData("required")]
[InlineData("auto")]
[InlineData("something_custom")]
public void SerializesWithMode(string? mode)
{
ToolChoice toolChoice = new() { Mode = mode };
var json = JsonSerializer.Serialize(toolChoice, McpJsonUtilities.DefaultOptions);
var deserialized = JsonSerializer.Deserialize<ToolChoice>(json, McpJsonUtilities.DefaultOptions);
Assert.NotNull(deserialized);
Assert.Equal(mode, deserialized.Mode);
}
}