Integrate Microsoft.Extensions.AI as a foundational building block (#94)
* Integrate Microsoft.Extensions.AI as a foundational building block As with logging abstractions coming from M.E.Logging and DI abstractions coming from M.E.DependencyInjection, M.E.AI provides the relevant AI abstractions. - Adds AsAIFunction and GetAIFunctionsAsync methods for getting functions for tools via a client. - Uses AIFunctionFactory to implement McpServerBuilderExtensions.WithTools. - Adds a RequestSamplingAsync overload that accepts ChatMessages/Options. - Adds an AsSamplingChatClient method that provides an IChatClient usable on the server to request sampling of the client. - Adds a CreateSamplingHandler method to enable an IChatClient to be easily used to satisfy sampling requests on the client. - Deprecate McpDotNet.Extensions.AI. * Address PR feedback
This commit is contained in:
parent
a0afc28ad6
commit
4597bbd7c2
8 changed files with 801 additions and 927 deletions
|
|
@ -1,95 +0,0 @@
|
|||
using System.Text.Json;
|
||||
using McpDotNet.Protocol.Types;
|
||||
|
||||
namespace McpDotNet.Extensions.AI.Tests;
|
||||
|
||||
public class AIFunctionUtilitiesTests
|
||||
{
|
||||
[Fact]
|
||||
public void MapToJsonElement_WithRequiredProperties_CreatesCorrectSchema()
|
||||
{
|
||||
// Arrange
|
||||
var tool = new Tool()
|
||||
{
|
||||
Name = "calculator",
|
||||
Description = "A simple calculator",
|
||||
InputSchema = new JsonSchema
|
||||
{
|
||||
Properties = new Dictionary<string, JsonSchemaProperty>
|
||||
{
|
||||
["a"] = new JsonSchemaProperty { Type = "number" },
|
||||
["b"] = new JsonSchemaProperty { Type = "number" }
|
||||
},
|
||||
Required = ["a", "b"]
|
||||
}
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = AIFunctionUtilities.MapToJsonElement(tool);
|
||||
|
||||
// Assert
|
||||
var resultObj = JsonSerializer.Deserialize<JsonElement>(result.GetRawText());
|
||||
Assert.Equal("object", resultObj.GetProperty("type").GetString());
|
||||
Assert.Equal("calculator", resultObj.GetProperty("title").GetString());
|
||||
Assert.Equal("A simple calculator", resultObj.GetProperty("description").GetString());
|
||||
// Add more detailed property assertions
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MapToJsonElement_WithOptionalProperties_CreatesCorrectSchema()
|
||||
{
|
||||
// Arrange
|
||||
var tool = new Tool()
|
||||
{
|
||||
Name = "format",
|
||||
Description = "Text formatter",
|
||||
InputSchema = new JsonSchema
|
||||
{
|
||||
Properties = new Dictionary<string, JsonSchemaProperty>
|
||||
{
|
||||
["text"] = new JsonSchemaProperty { Type = "string" },
|
||||
["style"] = new JsonSchemaProperty { Type = "string" }
|
||||
},
|
||||
Required = ["text"] // only text is required
|
||||
}
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = AIFunctionUtilities.MapToJsonElement(tool);
|
||||
|
||||
// Assert
|
||||
var resultObj = JsonSerializer.Deserialize<JsonElement>(result.GetRawText());
|
||||
Assert.Equal("object", resultObj.GetProperty("type").GetString());
|
||||
Assert.Equal("format", resultObj.GetProperty("title").GetString());
|
||||
Assert.Equal("Text formatter", resultObj.GetProperty("description").GetString());
|
||||
Assert.Contains("text", resultObj.GetProperty("required").EnumerateArray().Select(x => x.GetString()));
|
||||
Assert.DoesNotContain("style", resultObj.GetProperty("required").EnumerateArray().Select(x => x.GetString()));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MapToJsonElement_WithNullProperties_HandlesGracefully()
|
||||
{
|
||||
// Arrange
|
||||
var tool = new Tool()
|
||||
{
|
||||
Name = "minimal",
|
||||
Description = "Minimal tool",
|
||||
InputSchema = new JsonSchema
|
||||
{
|
||||
Properties = null,
|
||||
Required = null
|
||||
}
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = AIFunctionUtilities.MapToJsonElement(tool);
|
||||
|
||||
// Assert
|
||||
var resultObj = JsonSerializer.Deserialize<JsonElement>(result.GetRawText());
|
||||
Assert.Equal("object", resultObj.GetProperty("type").GetString());
|
||||
Assert.Equal("minimal", resultObj.GetProperty("title").GetString());
|
||||
Assert.Equal("Minimal tool", resultObj.GetProperty("description").GetString());
|
||||
Assert.Equal("{}", resultObj.GetProperty("properties").GetRawText());
|
||||
Assert.Empty(resultObj.GetProperty("required").EnumerateArray());
|
||||
}
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
global using Xunit;
|
||||
|
|
@ -1,112 +0,0 @@
|
|||
using McpDotNet.Client;
|
||||
using McpDotNet.Configuration;
|
||||
using Microsoft.Extensions.AI;
|
||||
using OpenAI;
|
||||
|
||||
namespace McpDotNet.Extensions.AI.Tests;
|
||||
|
||||
[Trait("Execution", "Manual")]
|
||||
public class IntegrationTests
|
||||
{
|
||||
private string _openAIKey = Environment.GetEnvironmentVariable("Provide your own key when running the test. Do not commit it.")!;
|
||||
|
||||
private static McpServerConfig GetEverythingServerConfig()
|
||||
{
|
||||
var config = new McpServerConfig
|
||||
{
|
||||
Id = "everything",
|
||||
Name = "Everything",
|
||||
TransportType = "stdio",
|
||||
TransportOptions = new Dictionary<string, string>
|
||||
{
|
||||
["command"] = "npx",
|
||||
["arguments"] = "-y @modelcontextprotocol/server-everything",
|
||||
}
|
||||
};
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
private static async Task<IMcpClient> GetMcpClientAsync()
|
||||
{
|
||||
McpClientOptions options = new()
|
||||
{
|
||||
ClientInfo = new() { Name = "McpDotNet.Extensions.AI.Tests", Version = "1.0.0" }
|
||||
};
|
||||
|
||||
return await McpClientFactory.CreateAsync(GetEverythingServerConfig(), options);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task IntegrateWithMeai_UsingEverythingServer_ToolsAreProperlyCalled()
|
||||
{
|
||||
await using var client = await GetMcpClientAsync();
|
||||
var mappedTools = await client.ListToolsAsync().Select(t => t.ToAITool(client)).ToListAsync();
|
||||
|
||||
IChatClient openaiClient = new OpenAIClient(_openAIKey)
|
||||
.AsChatClient("gpt-4o-mini");
|
||||
|
||||
IChatClient chatClient = new ChatClientBuilder(openaiClient)
|
||||
.UseFunctionInvocation()
|
||||
.Build();
|
||||
|
||||
// Create message list
|
||||
IList<ChatMessage> messages =
|
||||
[
|
||||
// Add a system message
|
||||
new(ChatRole.System, "You are a helpful assistant, helping us test MCP server functionality."),
|
||||
];
|
||||
// If MCP server provides instructions, add them as an additional system message (you could also add it as a content part)
|
||||
if (!string.IsNullOrEmpty(client.ServerInstructions))
|
||||
{
|
||||
messages.Add(new(ChatRole.System, client.ServerInstructions));
|
||||
}
|
||||
// Add a user message
|
||||
messages.Add(new(ChatRole.User, "Please call the echo tool with the string 'Hello MCP!' and output the response ad verbatim."));
|
||||
|
||||
// Call the chat client
|
||||
Console.WriteLine("Asking GPT-4o-mini to call the Echo Tool...");
|
||||
var response = await chatClient.GetResponseAsync(
|
||||
messages,
|
||||
new() { Tools = mappedTools, Temperature = 0 });
|
||||
|
||||
// Assert
|
||||
Assert.Equal("Echo: Hello MCP!", response.Text);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task IntegrateWithMeai_UsingEverythingServer_AndSessionScope_ToolsAreProperlyCalled()
|
||||
{
|
||||
await using var sessionScope = await McpSessionScope.CreateAsync(GetEverythingServerConfig());
|
||||
|
||||
IChatClient openaiClient = new OpenAIClient(_openAIKey)
|
||||
.AsChatClient("gpt-4o-mini");
|
||||
|
||||
IChatClient chatClient = new ChatClientBuilder(openaiClient)
|
||||
.UseFunctionInvocation()
|
||||
.Build();
|
||||
|
||||
// Create message list
|
||||
IList<ChatMessage> messages =
|
||||
[
|
||||
// Add a system message
|
||||
new(ChatRole.System, "You are a helpful assistant, helping us test MCP server functionality."),
|
||||
];
|
||||
// If MCP server provides instructions, add them as an additional system message (you could also add it as a content part)
|
||||
foreach (var serverInstruction in sessionScope.ServerInstructions)
|
||||
{
|
||||
messages.Add(new(ChatRole.System, serverInstruction));
|
||||
}
|
||||
// Add a user message
|
||||
messages.Add(new(ChatRole.User, "Please call the echo tool with the string 'Hello MCP!' and output the response ad verbatim."));
|
||||
|
||||
// Call the chat client
|
||||
Console.WriteLine("Asking GPT-4o-mini to call the Echo Tool...");
|
||||
var response = await chatClient.GetResponseAsync(
|
||||
messages,
|
||||
new() { Tools = sessionScope.Tools, Temperature = 0 });
|
||||
|
||||
// Assert
|
||||
Assert.Equal("Echo: Hello MCP!", response.Text);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,35 +0,0 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net8.0;net472</TargetFrameworks>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<LangVersion>Latest</LangVersion>
|
||||
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
||||
<PackageReference Include="Moq" Version="4.20.72" />
|
||||
<PackageReference Include="System.Linq.AsyncEnumerable" Version="10.0.0-preview.1.25080.5" />
|
||||
<PackageReference Include="xunit" Version="2.9.2" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.0.0">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.3">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\McpDotNet.Extensions.AI\McpDotNet.Extensions.AI.csproj" />
|
||||
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" Version="9.3.0-preview.1.25161.3" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
@ -3,12 +3,16 @@ using McpDotNet.Configuration;
|
|||
using McpDotNet.Protocol.Messages;
|
||||
using McpDotNet.Protocol.Transport;
|
||||
using McpDotNet.Protocol.Types;
|
||||
using Microsoft.Extensions.AI;
|
||||
using OpenAI;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace McpDotNet.Tests;
|
||||
|
||||
public class ClientIntegrationTests : IClassFixture<ClientIntegrationTestFixture>
|
||||
{
|
||||
private static readonly string? s_openAIKey = Environment.GetEnvironmentVariable("AI:OpenAI:ApiKey")!;
|
||||
|
||||
private readonly ClientIntegrationTestFixture _fixture;
|
||||
|
||||
public ClientIntegrationTests(ClientIntegrationTestFixture fixture)
|
||||
|
|
@ -58,10 +62,12 @@ public class ClientIntegrationTests : IClassFixture<ClientIntegrationTestFixture
|
|||
// act
|
||||
await using var client = await _fixture.CreateClientAsync(clientId);
|
||||
var tools = await client.ListToolsAsync().ToListAsync();
|
||||
var aiFunctions = await client.GetAIFunctionsAsync();
|
||||
|
||||
// assert
|
||||
Assert.NotEmpty(tools);
|
||||
// We could add more specific assertions about expected tools
|
||||
Assert.NotEmpty(aiFunctions);
|
||||
Assert.Equal(tools.Count, aiFunctions.Count);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
|
|
@ -88,6 +94,23 @@ public class ClientIntegrationTests : IClassFixture<ClientIntegrationTestFixture
|
|||
Assert.Equal("Echo: Hello MCP!", textContent.Text);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(GetClients))]
|
||||
public async Task CallTool_Stdio_ViaAIFunction_EchoServer(string clientId)
|
||||
{
|
||||
// arrange
|
||||
|
||||
// act
|
||||
await using var client = await _fixture.CreateClientAsync(clientId);
|
||||
var aiFunctions = await client.GetAIFunctionsAsync();
|
||||
var echo = aiFunctions.Single(t => t.Name == "echo");
|
||||
var result = await echo.InvokeAsync([new KeyValuePair<string, object?>("message", "Hello MCP!")]);
|
||||
|
||||
// assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Contains("Echo: Hello MCP!", result.ToString());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(GetClients))]
|
||||
public async Task ListPrompts_Stdio(string clientId)
|
||||
|
|
@ -444,4 +467,69 @@ public class ClientIntegrationTests : IClassFixture<ClientIntegrationTestFixture
|
|||
|
||||
await client.DisposeAsync();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetAIFunctionsAsync_UsingEverythingServer_ToolsAreProperlyCalled()
|
||||
{
|
||||
if (s_openAIKey is null)
|
||||
{
|
||||
return; // Skip the test if the OpenAI key is not provided
|
||||
}
|
||||
|
||||
// Get the MCP client and tools from it.
|
||||
await using var client = await McpClientFactory.CreateAsync(_fixture.EverythingServerConfig, _fixture.DefaultOptions); ;
|
||||
var mappedTools = await client.GetAIFunctionsAsync();
|
||||
|
||||
// Create the chat client.
|
||||
using IChatClient chatClient = new OpenAIClient(s_openAIKey).AsChatClient("gpt-4o-mini")
|
||||
.AsBuilder()
|
||||
.UseFunctionInvocation()
|
||||
.Build();
|
||||
|
||||
// Create the messages.
|
||||
List<ChatMessage> messages = [new(ChatRole.System, "You are a helpful assistant.")];
|
||||
if (client.ServerInstructions is not null)
|
||||
{
|
||||
messages.Add(new(ChatRole.System, client.ServerInstructions));
|
||||
}
|
||||
messages.Add(new(ChatRole.User, "Please call the echo tool with the string 'Hello MCP!' and output the response ad verbatim."));
|
||||
|
||||
// Call the chat client
|
||||
var response = await chatClient.GetResponseAsync(messages, new() { Tools = [.. mappedTools], Temperature = 0 });
|
||||
|
||||
// Assert
|
||||
Assert.Equal("Echo: Hello MCP!", response.Text);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SamplingViaChatClient_RequestResponseProperlyPropagated()
|
||||
{
|
||||
if (s_openAIKey is null)
|
||||
{
|
||||
return; // Skip the test if the OpenAI key is not provided
|
||||
}
|
||||
|
||||
await using var client = await McpClientFactory.CreateAsync(_fixture.EverythingServerConfig, new()
|
||||
{
|
||||
ClientInfo = new() { Name = nameof(SamplingViaChatClient_RequestResponseProperlyPropagated), Version = "1.0.0" },
|
||||
Capabilities = new()
|
||||
{
|
||||
Sampling = new()
|
||||
{
|
||||
SamplingHandler = new OpenAIClient(s_openAIKey).AsChatClient("gpt-4o-mini").CreateSamplingHandler(),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
var result = await client.CallToolAsync("sampleLLM", new()
|
||||
{
|
||||
["prompt"] = "In just a few words, what is the most famous tower in Paris?",
|
||||
});
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
using System.ComponentModel;
|
||||
using System.Text.Json;
|
||||
using System.Text.RegularExpressions;
|
||||
using McpDotNet.Configuration;
|
||||
using McpDotNet.Server;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
|
@ -108,10 +109,7 @@ public class McpServerBuilderExtensionsToolsTests
|
|||
var result = await options.CallToolHandler!(new(Mock.Of<IMcpServer>(), new() { Name = "ReturnNull" }), CancellationToken.None);
|
||||
Assert.NotNull(result);
|
||||
Assert.NotNull(result.Content);
|
||||
Assert.NotEmpty(result.Content);
|
||||
|
||||
Assert.Equal("null", result.Content[0].Text);
|
||||
Assert.Empty(result.Content[0].Type);
|
||||
Assert.Empty(result.Content);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -127,7 +125,7 @@ public class McpServerBuilderExtensionsToolsTests
|
|||
Assert.NotNull(result.Content);
|
||||
Assert.NotEmpty(result.Content);
|
||||
|
||||
Assert.Equal("{\"SomeProp\": false}", result.Content[0].Text);
|
||||
Assert.Equal("{\"SomeProp\":false}", Regex.Replace(result.Content[0].Text ?? string.Empty, "\\s+", ""));
|
||||
Assert.Equal("text", result.Content[0].Type);
|
||||
}
|
||||
|
||||
|
|
@ -202,45 +200,18 @@ public class McpServerBuilderExtensionsToolsTests
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Throws_Exception_When_Tool_Fails()
|
||||
public async Task Returns_IsError_Content_When_Tool_Fails()
|
||||
{
|
||||
_builder.Object.WithTools(typeof(EchoTool));
|
||||
|
||||
var serviceProvider = _services.BuildServiceProvider();
|
||||
var options = serviceProvider.GetRequiredService<IOptions<McpServerHandlers>>().Value;
|
||||
|
||||
var action = async () => await options.CallToolHandler!(new(Mock.Of<IMcpServer>(), new() { Name = "ReturnError" }), CancellationToken.None);
|
||||
|
||||
var exception = await Assert.ThrowsAsync<InvalidOperationException>(action);
|
||||
Assert.Equal("Test error", exception.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Can_Call_Registered_Tool_With_Dependency_Injection()
|
||||
{
|
||||
bool dependencyInjectionCalled = false;
|
||||
_services.AddTransient(sp =>
|
||||
{
|
||||
dependencyInjectionCalled = true;
|
||||
return Mock.Of<IDependendService>();
|
||||
});
|
||||
|
||||
_builder.Object.WithTool<EchoToolWithDi>();
|
||||
|
||||
var serviceProvider = _services.BuildServiceProvider();
|
||||
var options = serviceProvider.GetRequiredService<IOptions<McpServerHandlers>>().Value;
|
||||
|
||||
var mcpServer = new Mock<IMcpServer>();
|
||||
mcpServer.SetupGet(s => s.ServiceProvider).Returns(serviceProvider);
|
||||
|
||||
var result = await options.CallToolHandler!(new(mcpServer.Object, new() { Name = "Echo", Arguments = new() { { "message", "Peter" } } }), CancellationToken.None);
|
||||
Assert.NotNull(result);
|
||||
Assert.NotNull(result.Content);
|
||||
Assert.NotEmpty(result.Content);
|
||||
|
||||
Assert.Equal("hello Peter", result.Content[0].Text);
|
||||
|
||||
Assert.True(dependencyInjectionCalled);
|
||||
var response = await options.CallToolHandler!(new(Mock.Of<IMcpServer>(), new() { Name = nameof(EchoTool.ThrowException) }), CancellationToken.None);
|
||||
Assert.True(response.IsError);
|
||||
Assert.NotNull(response.Content);
|
||||
Assert.NotEmpty(response.Content);
|
||||
Assert.Contains("Test error", response.Content[0].Text);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -252,37 +223,32 @@ public class McpServerBuilderExtensionsToolsTests
|
|||
var options = serviceProvider.GetRequiredService<IOptions<McpServerHandlers>>().Value;
|
||||
|
||||
var exception = await Assert.ThrowsAsync<McpServerException>(async () => await options.CallToolHandler!(new(Mock.Of<IMcpServer>(), new() { Name = "NotRegisteredTool" }), CancellationToken.None));
|
||||
Assert.Equal("Unknown tool: NotRegisteredTool", exception.Message);
|
||||
Assert.Contains("'NotRegisteredTool'", exception.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Throws_Exception_Missing_Parameter()
|
||||
{
|
||||
_builder.Object.WithTools(typeof(EchoTool));
|
||||
// TODO: https://github.com/dotnet/extensions/issues/6124
|
||||
//[Fact]
|
||||
//public async Task Throws_Exception_Missing_Parameter()
|
||||
//{
|
||||
// _builder.Object.WithTools(typeof(EchoTool));
|
||||
|
||||
var serviceProvider = _services.BuildServiceProvider();
|
||||
var options = serviceProvider.GetRequiredService<IOptions<McpServerHandlers>>().Value;
|
||||
// var serviceProvider = _services.BuildServiceProvider();
|
||||
// var options = serviceProvider.GetRequiredService<IOptions<McpServerHandlers>>().Value;
|
||||
|
||||
var exception = await Assert.ThrowsAsync<McpServerException>(async () => await options.CallToolHandler!(new(Mock.Of<IMcpServer>(), new() { Name = "Echo" }), CancellationToken.None));
|
||||
Assert.Equal("Missing required argument 'message'.", exception.Message);
|
||||
}
|
||||
// var exception = await Assert.ThrowsAsync<McpServerException>(async () => await options.CallToolHandler!(new(Mock.Of<IMcpServer>(), new() { Name = "Echo" }), CancellationToken.None));
|
||||
// Assert.Equal("Missing required argument 'message'.", exception.Message);
|
||||
//}
|
||||
|
||||
[Fact]
|
||||
public void Throws_Exception_For_Null_Types()
|
||||
{
|
||||
var action = () => _builder.Object.WithTools(toolTypes: null!);
|
||||
|
||||
var exception = Assert.Throws<ArgumentException>(action);
|
||||
Assert.Equal("At least one tool type must be provided. (Parameter 'toolTypes')", exception.Message);
|
||||
Assert.Throws<ArgumentNullException>("toolTypes", () => _builder.Object.WithTools(toolTypes: null!));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Throws_Exception_For_Empty_Types()
|
||||
public void Empty_Types_Is_Allowed()
|
||||
{
|
||||
var action = () => _builder.Object.WithTools(toolTypes: []);
|
||||
|
||||
var exception = Assert.Throws<ArgumentException>(action);
|
||||
Assert.Equal("At least one tool type must be provided. (Parameter 'toolTypes')", exception.Message);
|
||||
_builder.Object.WithTools(toolTypes: []); // no exception
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -302,12 +268,9 @@ public class McpServerBuilderExtensionsToolsTests
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public void Throws_Exception_When_No_Tools_Are_Found_In_Given_Assembly()
|
||||
public void Ok_If_No_Tools_Are_Found_In_Given_Assembly()
|
||||
{
|
||||
var action = () => _builder.Object.WithToolsFromAssembly(typeof(Mock).Assembly);
|
||||
|
||||
var exception = Assert.Throws<ArgumentException>(action);
|
||||
Assert.Equal("No types with marked methods found in the assembly. (Parameter 'assembly')", exception.Message);
|
||||
_builder.Object.WithToolsFromAssembly(typeof(Mock).Assembly);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -324,14 +287,14 @@ public class McpServerBuilderExtensionsToolsTests
|
|||
|
||||
var tool = result.Tools.First(t => t.Name == "TestTool");
|
||||
Assert.Equal("TestTool", tool.Name);
|
||||
Assert.Null(tool.Description);
|
||||
Assert.Empty(tool.Description);
|
||||
Assert.NotNull(tool.InputSchema);
|
||||
Assert.Equal("object", tool.InputSchema.Type);
|
||||
Assert.NotNull(tool.InputSchema.Properties);
|
||||
Assert.NotEmpty(tool.InputSchema.Properties);
|
||||
|
||||
Assert.Contains("number", tool.InputSchema.Properties);
|
||||
Assert.Equal("number", tool.InputSchema.Properties["number"].Type);
|
||||
Assert.Equal("integer", tool.InputSchema.Properties["number"].Type);
|
||||
|
||||
Assert.Contains("otherNumber", tool.InputSchema.Properties);
|
||||
Assert.Equal("number", tool.InputSchema.Properties["otherNumber"].Type);
|
||||
|
|
@ -398,7 +361,7 @@ public class McpServerBuilderExtensionsToolsTests
|
|||
}
|
||||
|
||||
[McpTool]
|
||||
public static string ReturnError()
|
||||
public static string ThrowException()
|
||||
{
|
||||
throw new InvalidOperationException("Test error");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,8 +4,10 @@ using McpDotNet.Protocol.Transport;
|
|||
using McpDotNet.Protocol.Types;
|
||||
using McpDotNet.Server;
|
||||
using McpDotNet.Tests.Utils;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Moq;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace McpDotNet.Tests.Server;
|
||||
|
||||
|
|
@ -98,8 +100,7 @@ public class McpServerTests
|
|||
server.GetType().GetField("_isInitializing", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)?.SetValue(server, true);
|
||||
|
||||
// Act & Assert
|
||||
var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => server.StartAsync());
|
||||
Assert.Equal("Server is already initializing", exception.Message);
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(() => server.StartAsync());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -159,8 +160,7 @@ public class McpServerTests
|
|||
var action = () => server.RequestSamplingAsync(new CreateMessageRequestParams { Messages = [] }, CancellationToken.None);
|
||||
|
||||
// Act & Assert
|
||||
var exception = await Assert.ThrowsAsync<McpServerException>(action);
|
||||
Assert.Equal("Client does not support sampling", exception.Message);
|
||||
await Assert.ThrowsAsync<ArgumentException>("server", action);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -190,8 +190,7 @@ public class McpServerTests
|
|||
server.ClientCapabilities = new ClientCapabilities();
|
||||
|
||||
// Act & Assert
|
||||
var exception = await Assert.ThrowsAsync<McpServerException>(() => server.RequestRootsAsync(new ListRootsRequestParams(), CancellationToken.None));
|
||||
Assert.Equal("Client does not support roots", exception.Message);
|
||||
await Assert.ThrowsAsync<ArgumentException>("server", () => server.RequestRootsAsync(new ListRootsRequestParams(), CancellationToken.None));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -222,8 +221,7 @@ public class McpServerTests
|
|||
|
||||
var action = async () => await server.RequestRootsAsync(new ListRootsRequestParams(), CancellationToken.None);
|
||||
|
||||
var exception = await Assert.ThrowsAsync<McpClientException>(action);
|
||||
Assert.Equivalent("Transport is not connected", exception.Message);
|
||||
await Assert.ThrowsAsync<McpClientException>(action);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -563,4 +561,89 @@ public class McpServerTests
|
|||
|
||||
Assert.Throws<McpServerException>(() => McpServerFactory.Create(transport, options, _loggerFactory.Object, _serviceProvider));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AsSamplingChatClient_NoSamplingSupport_Throws()
|
||||
{
|
||||
await using var server = new TestServerForIChatClient(supportsSampling: false);
|
||||
|
||||
Assert.Throws<ArgumentException>("server", () => server.AsSamplingChatClient());
|
||||
}
|
||||
|
||||
|
||||
[Fact]
|
||||
public async Task AsSamplingChatClient_HandlesRequestResponse()
|
||||
{
|
||||
await using var server = new TestServerForIChatClient(supportsSampling: true);
|
||||
|
||||
IChatClient client = server.AsSamplingChatClient();
|
||||
|
||||
ChatMessage[] messages =
|
||||
[
|
||||
new (ChatRole.System, "You are a helpful assistant."),
|
||||
new (ChatRole.User, "I am going to France."),
|
||||
new (ChatRole.User, "What is the most famous tower in Paris?"),
|
||||
new (ChatRole.System, "More system stuff."),
|
||||
];
|
||||
|
||||
ChatResponse response = await client.GetResponseAsync(messages, new ChatOptions
|
||||
{
|
||||
Temperature = 0.75f,
|
||||
MaxOutputTokens = 42,
|
||||
StopSequences = ["."],
|
||||
});
|
||||
|
||||
Assert.Equal("amazingmodel", response.ModelId);
|
||||
Assert.Equal(ChatFinishReason.Stop, response.FinishReason);
|
||||
Assert.Single(response.Messages);
|
||||
Assert.Equal("The Eiffel Tower.", response.Text);
|
||||
Assert.Equal(ChatRole.Assistant, response.Messages[0].Role);
|
||||
}
|
||||
|
||||
private sealed class TestServerForIChatClient(bool supportsSampling) : IMcpServer
|
||||
{
|
||||
public ClientCapabilities? ClientCapabilities =>
|
||||
supportsSampling ? new ClientCapabilities { Sampling = new SamplingCapability() } :
|
||||
null;
|
||||
|
||||
public async Task<T> SendRequestAsync<T>(JsonRpcRequest request, CancellationToken cancellationToken) where T : class
|
||||
{
|
||||
CreateMessageRequestParams rp = Assert.IsType<CreateMessageRequestParams>(request.Params);
|
||||
|
||||
Assert.Equal(0.75f, rp.Temperature);
|
||||
Assert.Equal(42, rp.MaxTokens);
|
||||
Assert.Equal(["."], rp.StopSequences);
|
||||
Assert.Null(rp.IncludeContext);
|
||||
Assert.Null(rp.Metadata);
|
||||
Assert.Null(rp.ModelPreferences);
|
||||
|
||||
Assert.Equal($"You are a helpful assistant.{Environment.NewLine}More system stuff.", rp.SystemPrompt);
|
||||
|
||||
Assert.Equal(2, rp.Messages.Count);
|
||||
Assert.Equal("I am going to France.", rp.Messages[0].Content.Text);
|
||||
Assert.Equal("What is the most famous tower in Paris?", rp.Messages[1].Content.Text);
|
||||
|
||||
CreateMessageResult result = new()
|
||||
{
|
||||
Content = new() { Text = "The Eiffel Tower.", Type = "text" },
|
||||
Model = "amazingmodel",
|
||||
Role = "assistant",
|
||||
StopReason = "endTurn",
|
||||
};
|
||||
return (T)(object)result;
|
||||
}
|
||||
|
||||
public ValueTask DisposeAsync() => default;
|
||||
|
||||
public bool IsInitialized => throw new NotImplementedException();
|
||||
|
||||
public Implementation? ClientInfo => throw new NotImplementedException();
|
||||
public IServiceProvider? ServiceProvider => throw new NotImplementedException();
|
||||
public void AddNotificationHandler(string method, Func<JsonRpcNotification, Task> handler) =>
|
||||
throw new NotImplementedException();
|
||||
public Task SendMessageAsync(IJsonRpcMessage message, CancellationToken cancellationToken = default) =>
|
||||
throw new NotImplementedException();
|
||||
public Task StartAsync(CancellationToken cancellationToken = default) =>
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@
|
|||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" Version="9.3.0-preview.1.25161.3" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
||||
|
|
|
|||
Loading…
Reference in a new issue