Microsoft.Extensions.AI.Abstractions integration (#25)
* MEAI integration * README and xmldocs
This commit is contained in:
parent
502a09d9fc
commit
89c4913ed7
13 changed files with 589 additions and 1 deletions
|
|
@ -12,6 +12,13 @@ For more information about MCP:
|
|||
- [Protocol Specification](https://spec.modelcontextprotocol.io/)
|
||||
- [GitHub Organization](https://github.com/modelcontextprotocol)
|
||||
|
||||
## Available Packages
|
||||
|
||||
| Package | Description | Documentation |
|
||||
|---------|-------------|---------------|
|
||||
| [mcpdotnet](src/mcpdotnet) | Core MCP implementation for .NET | [README](README.md) |
|
||||
| [McpDotNet.Extensions.AI](src/McpDotNet.Extensions.AI) | Integration with Microsoft.Extensions.AI | [README](src/McpDotNet.Extensions.AI/README.md) |
|
||||
|
||||
## Design Goals
|
||||
|
||||
This library aims to provide a clean, specification-compliant implementation of the MCP protocol, with minimal additional abstraction. While transport implementations necessarily include additional code, they follow patterns established by the official SDKs where possible.
|
||||
|
|
|
|||
|
|
@ -13,6 +13,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AnthropicToolsConsole", "sa
|
|||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "mcpdotnet.TestServer", "tests\mcpdotnet.TestServer\mcpdotnet.TestServer.csproj", "{7C229573-A085-4ECC-8131-958CDA2BE731}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "McpDotNet.Extensions.AI", "src\McpDotNet.Extensions.AI\McpDotNet.Extensions.AI.csproj", "{B3E019B8-3459-4D52-864E-703DDA2C02AD}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "McpDotNet.Extensions.AI.Tests", "tests\McpDotNet.Extensions.AI.Tests\McpDotNet.Extensions.AI.Tests.csproj", "{67BEF596-AD39-4BDD-B505-31DE7FF0C8D2}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
|
|
@ -39,6 +43,14 @@ Global
|
|||
{7C229573-A085-4ECC-8131-958CDA2BE731}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{7C229573-A085-4ECC-8131-958CDA2BE731}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{7C229573-A085-4ECC-8131-958CDA2BE731}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{B3E019B8-3459-4D52-864E-703DDA2C02AD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{B3E019B8-3459-4D52-864E-703DDA2C02AD}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{B3E019B8-3459-4D52-864E-703DDA2C02AD}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{B3E019B8-3459-4D52-864E-703DDA2C02AD}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{67BEF596-AD39-4BDD-B505-31DE7FF0C8D2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{67BEF596-AD39-4BDD-B505-31DE7FF0C8D2}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{67BEF596-AD39-4BDD-B505-31DE7FF0C8D2}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{67BEF596-AD39-4BDD-B505-31DE7FF0C8D2}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
using McpDotNet.Client;
|
||||
using McpDotNet.Protocol.Types;
|
||||
using Microsoft.Extensions.AI;
|
||||
using SimpleToolsConsole;
|
||||
|
||||
namespace SimpleToolsConsole;
|
||||
|
||||
|
|
|
|||
26
src/McpDotNet.Extensions.AI/AIFunctionUtilities.cs
Normal file
26
src/McpDotNet.Extensions.AI/AIFunctionUtilities.cs
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
using McpDotNet.Protocol.Types;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace McpDotNet.Extensions.AI;
|
||||
|
||||
internal static class AIFunctionUtilities
|
||||
{
|
||||
/// <summary>
|
||||
/// Maps a Tool to a JsonElement that represents the tool's schema (top-level and input schema/parameters).
|
||||
/// </summary>
|
||||
public static JsonElement MapToJsonElement(Tool tool)
|
||||
{
|
||||
// Create a JsonObject that matches the MEAI schema format
|
||||
var schemaObj = new Dictionary<string, object>
|
||||
{
|
||||
["type"] = "object",
|
||||
["title"] = tool.Name,
|
||||
["description"] = tool.Description ?? string.Empty,
|
||||
["properties"] = tool.InputSchema?.Properties ?? new(),
|
||||
["required"] = tool.InputSchema?.Required ?? []
|
||||
};
|
||||
|
||||
// Convert to JsonElement
|
||||
return JsonSerializer.SerializeToElement(schemaObj);
|
||||
}
|
||||
}
|
||||
57
src/McpDotNet.Extensions.AI/McpAIFunction.cs
Normal file
57
src/McpDotNet.Extensions.AI/McpAIFunction.cs
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
using McpDotNet.Client;
|
||||
using McpDotNet.Protocol.Types;
|
||||
using Microsoft.Extensions.AI;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace McpDotNet.Extensions.AI;
|
||||
|
||||
/// <summary>
|
||||
/// Represents an AI function that calls a tool through mcpdotnet.
|
||||
/// </summary>
|
||||
public class McpAIFunction : AIFunction
|
||||
{
|
||||
private readonly Tool _tool;
|
||||
private readonly IMcpClient _client;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="McpAIFunction"/> class.
|
||||
/// </summary>
|
||||
public McpAIFunction(Tool tool, IMcpClient client)
|
||||
{
|
||||
_tool = tool ?? throw new ArgumentNullException(nameof(tool));
|
||||
_client = client ?? throw new ArgumentNullException(nameof(client));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string ToString() => _tool.Name;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override JsonElement JsonSchema => AIFunctionUtilities.MapToJsonElement(_tool);
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected async override Task<object?> InvokeCoreAsync(IEnumerable<KeyValuePair<string, object?>> arguments, CancellationToken cancellationToken)
|
||||
{
|
||||
// Convert arguments to dictionary format expected by mcpdotnet
|
||||
Dictionary<string, object> argDict = new();
|
||||
foreach (var arg in arguments)
|
||||
{
|
||||
if (arg.Value is not null)
|
||||
{
|
||||
argDict[arg.Key] = arg.Value;
|
||||
}
|
||||
}
|
||||
|
||||
// Call the tool through mcpdotnet
|
||||
var result = await _client.CallToolAsync(
|
||||
_tool.Name,
|
||||
argDict.Count == 0 ? new() : argDict,
|
||||
cancellationToken: cancellationToken
|
||||
);
|
||||
|
||||
// Extract the text content from the result
|
||||
// For simplicity in this sample, we'll just concatenate all text content
|
||||
return string.Join("\n", result.Content
|
||||
.Where(c => c.Type == "text")
|
||||
.Select(c => c.Text));
|
||||
}
|
||||
}
|
||||
39
src/McpDotNet.Extensions.AI/McpDotNet.Extensions.AI.csproj
Normal file
39
src/McpDotNet.Extensions.AI/McpDotNet.Extensions.AI.csproj
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<!-- license and package properties -->
|
||||
<PackageId>McpDotNet.Extensions.AI</PackageId>
|
||||
<Version>1.0.1.1</Version>
|
||||
<Authors>PederHP</Authors>
|
||||
<Description>Microsoft.Extensions.AI integration for the Model Context Protocol (MCP). Enables seamless use of MCP tools as AI functions in any IChatClient implementation.</Description>
|
||||
<PackageProjectUrl>https://github.com/PederHP/mcpdotnet/src/McpDotNet.Extensions.AI</PackageProjectUrl>
|
||||
<RepositoryUrl>https://github.com/PederHP/mcpdotnet</RepositoryUrl>
|
||||
<RepositoryType>git</RepositoryType>
|
||||
<PackageTags>mcp;ai;microsoft-extensions-ai;chatclient;llm</PackageTags>
|
||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||
<!-- Assembly properties -->
|
||||
<AssemblyName>McpDotNet.Extensions.AI</AssemblyName>
|
||||
<RootNamespace>McpDotNet.Extensions.AI</RootNamespace>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<IncludeSymbols>true</IncludeSymbols>
|
||||
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<InternalsVisibleTo Include="McpDotNet.Extensions.AI.Tests" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.AI.Abstractions" Version="9.3.0-preview.1.25114.11" />
|
||||
<PackageReference Include="Microsoft.Extensions.AI" Version="9.3.0-preview.1.25114.11" />
|
||||
<PackageReference Include="mcpdotnet" Version="1.0.1.1" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
100
src/McpDotNet.Extensions.AI/McpSessionScope.cs
Normal file
100
src/McpDotNet.Extensions.AI/McpSessionScope.cs
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
using McpDotNet.Client;
|
||||
using McpDotNet.Configuration;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
|
||||
namespace McpDotNet.Extensions.AI;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a scope that manages a connection session with one or more MCP servers.
|
||||
/// Compatible with both stdio and SSE transports.
|
||||
///
|
||||
/// Disposing the scope will dispose all clients and tools, and close all connections.
|
||||
/// </summary>
|
||||
public class McpSessionScope : IAsyncDisposable
|
||||
{
|
||||
private readonly List<IMcpClient> _clients = new();
|
||||
private bool _disposed;
|
||||
|
||||
/// <summary>
|
||||
/// The list of tools exposed by the servers in this scope.
|
||||
/// </summary>
|
||||
public IList<AITool> Tools { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The server instructions provided by the servers in this scope.
|
||||
/// If a server does not provide instructions, they will not be included, so the list may be empty.
|
||||
/// </summary>
|
||||
public IReadOnlyList<string> ServerInstructions { get; private set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new session scope with a single server.
|
||||
/// </summary>
|
||||
/// <param name="serverConfig">A configuration object describing the MCP server.</param>
|
||||
/// <param name="options">An options object with the client name and capabilities. Passed to the server.</param>
|
||||
/// <param name="loggerFactory">A logger factory for mcpdotnet.</param>
|
||||
/// <returns>A session scope which will keep the connection alive until disposed.</returns>
|
||||
public static async Task<McpSessionScope> CreateAsync(McpServerConfig serverConfig,
|
||||
McpClientOptions? options = null,
|
||||
ILoggerFactory? loggerFactory = null)
|
||||
{
|
||||
var scope = new McpSessionScope();
|
||||
var client = await scope.AddClientAsync(serverConfig, options, loggerFactory);
|
||||
var tools = await client.ListToolsAsync();
|
||||
scope.Tools = tools.Tools.Select(t => new McpAIFunction(t, client)).ToList<AITool>();
|
||||
if (!string.IsNullOrEmpty(client.ServerInstructions))
|
||||
{
|
||||
scope.ServerInstructions = [client.ServerInstructions];
|
||||
}
|
||||
return scope;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new session scope with multiple servers.
|
||||
/// </summary>
|
||||
/// <param name="serverConfigs">Configuration objects describing the MCP servers.</param>
|
||||
/// <param name="options">An options object with the client name and capabilities. Passed to the servers.</param>
|
||||
/// <param name="loggerFactory">A logger factory for mcpdotnet.</param>
|
||||
/// <returns>A session scope which keep the connections alive until disposed.</returns>
|
||||
public static async Task<McpSessionScope> CreateAsync(IEnumerable<McpServerConfig> serverConfigs,
|
||||
McpClientOptions? options = null,
|
||||
ILoggerFactory? loggerFactory = null)
|
||||
{
|
||||
var scope = new McpSessionScope();
|
||||
foreach (var config in serverConfigs)
|
||||
{
|
||||
var client = await scope.AddClientAsync(config, options, loggerFactory);
|
||||
var tools = await client.ListToolsAsync();
|
||||
scope.Tools = scope.Tools.Concat(tools.Tools.Select(t => new McpAIFunction(t, client))).ToList();
|
||||
}
|
||||
scope.ServerInstructions = scope._clients.Select(c => c.ServerInstructions).Where(s => !string.IsNullOrEmpty(s)).ToList()!;
|
||||
return scope;
|
||||
}
|
||||
|
||||
private async Task<IMcpClient> AddClientAsync(McpServerConfig config,
|
||||
McpClientOptions? options,
|
||||
ILoggerFactory? loggerFactory = null)
|
||||
{
|
||||
var factory = new McpClientFactory([config],
|
||||
options ?? new() { ClientInfo = new() { Name = "AnonymousClient", Version = "1.0.0.0" } },
|
||||
loggerFactory ?? NullLoggerFactory.Instance);
|
||||
var client = await factory.GetClientAsync(config.Id);
|
||||
_clients.Add(client);
|
||||
return client;
|
||||
}
|
||||
|
||||
/// </inheritdoc>
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
if (_disposed) return;
|
||||
_disposed = true;
|
||||
|
||||
foreach (var client in _clients)
|
||||
{
|
||||
await client.DisposeAsync();
|
||||
}
|
||||
_clients.Clear();
|
||||
Tools = [];
|
||||
}
|
||||
}
|
||||
17
src/McpDotNet.Extensions.AI/McpToolExtensions.cs
Normal file
17
src/McpDotNet.Extensions.AI/McpToolExtensions.cs
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
using McpDotNet.Client;
|
||||
using McpDotNet.Protocol.Types;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace McpDotNet.Extensions.AI;
|
||||
|
||||
/// <summary>
|
||||
/// Utility class for converting an MCP Tool to an Microsoft.Extensions.AI.Abstractions AITool.
|
||||
/// </summary>
|
||||
public static class McpToolExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Converts an MCP Tool to an AITool.
|
||||
/// </summary>
|
||||
public static AITool ToAITool(this Tool tool, IMcpClient client)
|
||||
=> new McpAIFunction(tool, client);
|
||||
}
|
||||
83
src/McpDotNet.Extensions.AI/README.md
Normal file
83
src/McpDotNet.Extensions.AI/README.md
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
# McpDotNet.Extensions.AI
|
||||
[](https://www.nuget.org/packages/McpDotNet.Extensions.AI/)
|
||||
|
||||
Microsoft.Extensions.AI integration for the Model Context Protocol (MCP). Enables seamless use of MCP tools as AI functions in any IChatClient implementation. Built on top of [mcpdotnet](https://github.com/PederHP/mcpdotnet).
|
||||
|
||||
## About MCP
|
||||
|
||||
The Model Context Protocol (MCP) is an open protocol that standardizes how applications provide context to Large Language Models (LLMs). It enables secure integration between LLMs and various data sources and tools.
|
||||
|
||||
For more information about MCP:
|
||||
- [Official Documentation](https://modelcontextprotocol.io/)
|
||||
- [Protocol Specification](https://spec.modelcontextprotocol.io/)
|
||||
- [GitHub Organization](https://github.com/modelcontextprotocol)
|
||||
|
||||
## Design Goals
|
||||
|
||||
The goal of this library is to provide a simple and efficient way to integrate MCP capabilities with Microsoft.Extensions.AI.Abstractions. It is designed to be easy to use, wrapping boilerplate code and providing a clean API for AI developers to use leverage MCP capabilities without having to worry about the underlying protocol.
|
||||
|
||||
## Features
|
||||
|
||||
- Easy lifecycle management of MCP connections
|
||||
- Seamless mapping of MCP tools to Microsoft.Extensions.AI.Abstractions AIFunction objects
|
||||
- Use convenience methods, while still having access to the full MCP protocol through the underlying mcpdotnet library
|
||||
- Compatible with .NET 8.0 and later
|
||||
|
||||
## Getting Started (Client)
|
||||
|
||||
To use McpDotNet.Extensions.AI, first install it via NuGet:
|
||||
|
||||
```powershell
|
||||
dotnet add package McpDotNet.Extensions.AI
|
||||
```
|
||||
|
||||
Create a configuration object for the MCP server you want to connect to:
|
||||
```csharp
|
||||
var config = new McpServerConfig
|
||||
{
|
||||
Id = "everything",
|
||||
Name = "Everything",
|
||||
TransportType = "stdio",
|
||||
TransportOptions = new Dictionary<string, string>
|
||||
{
|
||||
["command"] = "npx",
|
||||
["arguments"] = "-y @modelcontextprotocol/server-everything",
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
Then create an MCP session scope and use it with a chat client of your choice:
|
||||
```csharp
|
||||
await using var sessionScope = await McpSessionScope.CreateAsync(config);
|
||||
|
||||
IChatClient openaiClient = new OpenAIClient("sk-your-key")
|
||||
.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."),
|
||||
new(ChatRole.User, "Placeholder for the actual user message.")
|
||||
];
|
||||
|
||||
// Call the chat client
|
||||
var response = await chatClient.GetResponseAsync(messages,
|
||||
new() { Tools = sessionScope.Tools });
|
||||
```
|
||||
|
||||
As you can see, all you really need to do is create one or more configuration objects matching the MCP servers you want to use, create an MCP session scope, and then the tools exposed by the server(s) will be available to you through the `Tools` property of the `McpSessionScope` object.
|
||||
|
||||
The `McpSessionScope` object implements `IAsyncDisposable`, and managing the connection lifecycle is as simple as using the `await using` pattern. Note that you have to keep the `McpSessionScope` object alive for as long as you want to use the tools it provides.
|
||||
|
||||
## Roadmap
|
||||
|
||||
- Convenience methods for other capabilities: prompts, sampling, resources, etc.
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
||||
|
|
@ -0,0 +1,95 @@
|
|||
using McpDotNet.Protocol.Types;
|
||||
using System.Text.Json;
|
||||
|
||||
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.True(resultObj.GetProperty("properties").GetRawText() == "{}");
|
||||
Assert.Empty(resultObj.GetProperty("required").EnumerateArray());
|
||||
}
|
||||
}
|
||||
1
tests/McpDotNet.Extensions.AI.Tests/GlobalUsings.cs
Normal file
1
tests/McpDotNet.Extensions.AI.Tests/GlobalUsings.cs
Normal file
|
|
@ -0,0 +1 @@
|
|||
global using Xunit;
|
||||
119
tests/McpDotNet.Extensions.AI.Tests/IntegrationTests.cs
Normal file
119
tests/McpDotNet.Extensions.AI.Tests/IntegrationTests.cs
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
using McpDotNet.Client;
|
||||
using McpDotNet.Configuration;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using OpenAI;
|
||||
|
||||
namespace McpDotNet.Extensions.AI.Tests;
|
||||
|
||||
public class IntegrationTests
|
||||
{
|
||||
private const string OpenAIKey = ""; // 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" }
|
||||
};
|
||||
|
||||
var factory = new McpClientFactory(
|
||||
[GetEverythingServerConfig()],
|
||||
options,
|
||||
NullLoggerFactory.Instance
|
||||
);
|
||||
|
||||
return await factory.GetClientAsync("everything");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task IntegrateWithMeai_UsingEverythingServer_ToolsAreProperlyCalled()
|
||||
{
|
||||
var client = await GetMcpClientAsync();
|
||||
var tools = await client.ListToolsAsync();
|
||||
var mappedTools = tools.Tools.Select(t => t.ToAITool(client)).ToList();
|
||||
|
||||
IChatClient openaiClient = new OpenAIClient(OpenAIKey)
|
||||
.AsChatClient("gpt-4o-mini");
|
||||
|
||||
IChatClient chatClient = new ChatClientBuilder(openaiClient)
|
||||
.UseFunctionInvocation()
|
||||
.Build();
|
||||
|
||||
// Create message list
|
||||
IList<ChatMessage> messages = new List<ChatMessage>();
|
||||
// Add a system message
|
||||
messages.Add(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(4, messages.Count); // 1 system messages, 1 user message, 1 tool message, 1 response message
|
||||
Assert.Equal("Echo: Hello MCP!", response.Message.Contents[0].ToString());
|
||||
}
|
||||
|
||||
[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(4, messages.Count); // 1 system messages, 1 user message, 1 tool message, 1 response message
|
||||
Assert.Equal("Echo: Hello MCP!", response.Message.Contents[0].ToString());
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
|
||||
<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="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.25114.11" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
Loading…
Reference in a new issue