Reorganize folders, samples, minor bug fix (#14)

Reorganize folders
Samples
Casing fixes
This commit is contained in:
Peder Holdgaard Pedersen 2025-01-18 12:34:13 +01:00 • committed by GitHub
commit 3e35efe79c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
47 changed files with 501 additions and 20 deletions

View file

@ -80,6 +80,8 @@ Note that you should pass CancellationToken objects suitable for your use case,
It is also highly recommended that you pass a proper LoggerFactory instance to the factory constructor, to enable logging of MCP client operations.
You can find samples demonstrating how to use mcpdotnet with an LLM SDK in the [samples](samples) directory, and also refer to the [IntegrationTests](test/McpDotNet.IntegrationTests) project for more examples.
Additional examples and documentation will be added as in the near future.
## Roadmap
@ -89,7 +91,7 @@ Additional examples and documentation will be added as in the near future.
- Transport configuration
- Error handling and recovery
- Increase test coverage
- Add example applications
- Add additional samples and examples
- Performance optimization and profiling
## License

43
mcpdotnet.sln Normal file
View file

@ -0,0 +1,43 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.13.35507.96
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "mcpdotnet", "src\mcpdotnet\mcpdotnet.csproj", "{12260CD2-AFFC-4B2E-8898-F442CAA1FA0F}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "mcpdotnet.Tests", "tests\mcpdotnet.Tests\mcpdotnet.Tests.csproj", "{FF41F619-833D-4FA2-8C53-04D0A1D5AA61}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MEAIToolsConsole", "samples\microsoft.extensions.ai\tools\ToolsConsole\MEAIToolsConsole.csproj", "{76E295FA-9E85-7B99-332A-2CDBFCD5860A}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AnthropicToolsConsole", "samples\anthropic\tools\ToolsConsole\AnthropicToolsConsole.csproj", "{CA0BB450-1903-2F92-A68D-1285976551D6}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{12260CD2-AFFC-4B2E-8898-F442CAA1FA0F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{12260CD2-AFFC-4B2E-8898-F442CAA1FA0F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{12260CD2-AFFC-4B2E-8898-F442CAA1FA0F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{12260CD2-AFFC-4B2E-8898-F442CAA1FA0F}.Release|Any CPU.Build.0 = Release|Any CPU
{FF41F619-833D-4FA2-8C53-04D0A1D5AA61}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{FF41F619-833D-4FA2-8C53-04D0A1D5AA61}.Debug|Any CPU.Build.0 = Debug|Any CPU
{FF41F619-833D-4FA2-8C53-04D0A1D5AA61}.Release|Any CPU.ActiveCfg = Release|Any CPU
{FF41F619-833D-4FA2-8C53-04D0A1D5AA61}.Release|Any CPU.Build.0 = Release|Any CPU
{76E295FA-9E85-7B99-332A-2CDBFCD5860A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{76E295FA-9E85-7B99-332A-2CDBFCD5860A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{76E295FA-9E85-7B99-332A-2CDBFCD5860A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{76E295FA-9E85-7B99-332A-2CDBFCD5860A}.Release|Any CPU.Build.0 = Release|Any CPU
{CA0BB450-1903-2F92-A68D-1285976551D6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{CA0BB450-1903-2F92-A68D-1285976551D6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CA0BB450-1903-2F92-A68D-1285976551D6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CA0BB450-1903-2F92-A68D-1285976551D6}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {384A3888-751F-4D75-9AE5-587330582D89}
EndGlobalSection
EndGlobal

41
samples/README.md Normal file
View file

@ -0,0 +1,41 @@
# Samples
This directory contains example projects demonstrating how to use mcpdotnet with different LLM SDKs and platforms.
## Environment Setup
Before running the samples, you'll need to set up the following environment variables:
- `ANTHROPIC_API_KEY` - Required for the Anthropic sample
- `OPENAI_API_KEY` - Required for the MEAI/OpenAI sample
## Sample Projects
### Anthropic Integration
Located in `samples/anthropic`, this sample demonstrates integration with Anthropic's Claude API using mcpdotnet. The example shows how to:
- Configure an MCP client for use with Anthropic
- Map between MCP protocol types and Anthropic SDK types
- Handle tool invocations and responses
### Microsoft.Extensions.AI Integration
Located in `samples/microsoft.extensions.ai`, this sample shows how to use mcpdotnet with Microsoft's AI SDK. It demonstrates:
- Setting up an MCP client with MEAI
- Mapping MCP tools to MEAI function calls
- Handling streaming responses and tool invocations
## Implementation Notes
Each sample focuses on the mapping between MCP types and SDK-specific types, which is the primary integration point when working with different LLM SDKs. The samples provide reusable mapping code that can serve as a starting point for your own implementations.
The complexity of this mapping varies depending on how closely the SDK's design aligns with typical LLM APIs. The provided samples demonstrate best practices for both straightforward and more complex mapping scenarios.
## Running the Samples
1. Clone the repository
2. Set up the required environment variables
3. Navigate to the desired sample directory
4. Run `dotnet run` to execute the sample
For more detailed examples of mcpdotnet usage, you can also refer to the integration tests in the `tests/McpDotNet.IntegrationTests` project.

View file

@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Anthropic.SDK" Version="4.4.3" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\src\mcpdotnet\mcpdotnet.csproj" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,32 @@
using Anthropic.SDK.Common;
using System.Text.Json;
using System.Text.Json.Nodes;
public static class McpToolExtensions
{
public static IList<Anthropic.SDK.Common.Tool> ToAnthropicTools(this IEnumerable<McpDotNet.Protocol.Types.Tool> tools)
{
List<Anthropic.SDK.Common.Tool> result = new();
foreach (var tool in tools)
{
var function = tool.InputSchema == null
? new Function(tool.Name, tool.Description)
: new Function(tool.Name, tool.Description, JsonSerializer.Serialize(tool.InputSchema));
result.Add(function);
}
return result;
}
public static Dictionary<string, object>? ToMCPArguments(this JsonNode jsonNode)
{
if (jsonNode == null)
return null;
// Convert JsonNode to Dictionary<string, object>
return jsonNode.AsObject()
.ToDictionary(
prop => prop.Key,
prop => JsonSerializer.Deserialize<object>(prop.Value)!
);
}
}

View file

@ -0,0 +1,97 @@
using McpDotNet.Client;
using McpDotNet.Configuration;
using Microsoft.Extensions.Logging.Abstractions;
using Anthropic.SDK;
using Anthropic.SDK.Messaging;
using Anthropic.SDK.Constants;
internal class Program
{
private static async Task<IMcpClient> GetMcpClientAsync()
{
McpClientOptions options = new()
{
ClientInfo = new() { Name = "SimpleToolsConsole", Version = "1.0.0" }
};
var config = new McpServerConfig
{
Id = "everything",
Name = "Everything",
TransportType = "stdio",
TransportOptions = new Dictionary<string, string>
{
["command"] = "npx",
["arguments"] = "-y @modelcontextprotocol/server-everything",
}
};
var factory = new McpClientFactory(
[config],
options,
NullLoggerFactory.Instance
);
return await factory.GetClientAsync("everything");
}
private static async Task Main(string[] args)
{
try
{
Console.WriteLine("Initializing MCP 'everything' server");
var client = await GetMcpClientAsync();
Console.WriteLine("MCP 'everything' server initialized");
Console.WriteLine("Listing tools...");
var tools = await client.ListToolsAsync();
var anthropicTools = tools.Tools.ToAnthropicTools();
Console.WriteLine("Tools available:");
foreach (var tool in anthropicTools)
{
Console.WriteLine(" " + tool.Function.Name);
}
Console.WriteLine("Starting chat with Claude Haiku 3.5...");
using var antClient = new AnthropicClient(Environment.GetEnvironmentVariable("ANTHROPIC_API_KEY")!);
Console.WriteLine("Asking Claude to call the Echo Tool...");
var messages = new List<Message>
{
new Message(RoleType.User, "Please call the echo tool with the string 'Hello MCP!' and give me the response as-is.")
};
var parameters = new MessageParameters()
{
Messages = messages,
MaxTokens = 2048,
Model = AnthropicModels.Claude35Haiku,
Stream = false,
Temperature = 1.0m,
Tools = anthropicTools
};
var res = await antClient.Messages.GetClaudeMessageAsync(parameters);
messages.Add(res.Message);
foreach (var toolCall in res.ToolCalls)
{
var response = await client.CallToolAsync(toolCall.Name, toolCall.Arguments?.ToMCPArguments());
messages.Add(new Message(toolCall, response.Content[0].Text));
}
var finalResult = await antClient.Messages.GetClaudeMessageAsync(parameters);
Console.WriteLine("Final result: " + finalResult.Message.ToString());
Console.WriteLine();
Console.WriteLine("Chat with Claude Haiku 3.5 complete");
}
catch (Exception ex)
{
Console.WriteLine("Error occurred: " + ex.Message);
}
}
}

View file

@ -0,0 +1,7 @@
{
"profiles": {
"SimpleToolsConsole": {
"commandName": "Project"
}
}
}

View file

@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="mcpdotnet" Version="0.6.0.1" />
<PackageReference Include="Microsoft.Extensions.AI" Version="9.1.0-preview.1.25064.3" />
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" Version="9.1.0-preview.1.25064.3" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,75 @@
using McpDotNet.Client;
using McpDotNet.Protocol.Types;
using Microsoft.Extensions.AI;
using SimpleToolsConsole;
namespace SimpleToolsConsole;
public class McpAIFunction : AIFunction
{
private readonly Tool _tool;
private readonly IMcpClient _client;
private readonly AIFunctionMetadata _metadata;
public McpAIFunction(Tool tool, IMcpClient client)
{
_tool = tool ?? throw new ArgumentNullException(nameof(tool));
_client = client ?? throw new ArgumentNullException(nameof(client));
_metadata = new AIFunctionMetadata(tool.Name)
{
Description = tool.Description,
Parameters = MapFunctionParameters(tool.InputSchema),
ReturnParameter = new AIFunctionReturnParameterMetadata()
{
ParameterType = typeof(string),
}
};
}
public override string ToString() => _tool.Name;
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 ? null: 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));
}
public override AIFunctionMetadata Metadata => _metadata;
private static List<AIFunctionParameterMetadata> MapFunctionParameters(JsonSchema? jsonSchema)
{
var properties = jsonSchema?.Properties;
if (properties == null)
{
return new();
}
HashSet<string> requiredProperties = new(jsonSchema!.Required ?? []);
return properties.Select(kvp =>
new AIFunctionParameterMetadata(kvp.Key)
{
Description = kvp.Value.Description,
ParameterType = McpTypeMapper.MapJsonToDotNetType(kvp.Value.Type, requiredProperties.Contains(kvp.Key)),
IsRequired = requiredProperties.Contains(kvp.Key)
}).ToList();
}
}

View file

@ -0,0 +1,11 @@
using McpDotNet.Client;
using McpDotNet.Protocol.Types;
using Microsoft.Extensions.AI;
namespace SimpleToolsConsole;
public static class McpToolExtensions
{
public static AITool ToAITool(this Tool tool, IMcpClient client)
=> new McpAIFunction(tool, client);
}

View file

@ -0,0 +1,31 @@
// Enable nullable reference types, as we need this for dynamic mapping of JSON schema to .NET types
#nullable enable
namespace SimpleToolsConsole;
public static class McpTypeMapper
{
public static Type MapJsonToDotNetType(string jsonType, bool required)
{
// For simplicity, we don't handle complex types in this sample, but use object instead
var baseType = jsonType switch
{
"string" => typeof(string),
"integer" => typeof(int),
"number" => typeof(double),
"boolean" => typeof(bool),
"array" => typeof(IEnumerable<object>),
"object" => typeof(IDictionary<string, object>),
_ => typeof(object)
};
// If it's a value type and not required, make it nullable
if (!required && baseType.IsValueType)
{
return typeof(Nullable<>).MakeGenericType(baseType);
}
// Reference types are already nullable when not required
return baseType;
}
}

View file

@ -0,0 +1,85 @@
using McpDotNet.Client;
using McpDotNet.Configuration;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.Logging.Abstractions;
using OpenAI;
using SimpleToolsConsole;
internal class Program
{
private static async Task<IMcpClient> GetMcpClientAsync()
{
McpClientOptions options = new()
{
ClientInfo = new() { Name = "SimpleToolsConsole", Version = "1.0.0" }
};
var config = new McpServerConfig
{
Id = "everything",
Name = "Everything",
TransportType = "stdio",
TransportOptions = new Dictionary<string, string>
{
["command"] = "npx",
["arguments"] = "-y @modelcontextprotocol/server-everything",
}
};
var factory = new McpClientFactory(
[config],
options,
NullLoggerFactory.Instance
);
return await factory.GetClientAsync("everything");
}
private static async Task Main(string[] args)
{
try
{
Console.WriteLine("Initializing MCP 'everything' server");
var client = await GetMcpClientAsync();
Console.WriteLine("MCP 'everything' server initialized");
Console.WriteLine("Listing tools...");
var tools = await client.ListToolsAsync();
var mappedTools = tools.Tools.Select(t => t.ToAITool(client)).ToList();
Console.WriteLine("Tools available:");
foreach (var tool in mappedTools)
{
Console.WriteLine(" " + tool);
}
Console.WriteLine("Starting chat with GPT-4o-mini...");
// Note: We use then Microsoft.Extensions.AI.OpenAI client here, but it could be any other MEAI client.
// Provide your own OPENAI_API_KEY via an environment variable, secret or file-based appsettings. Do not hardcode it.
IChatClient openaiClient = new OpenAIClient(Environment.GetEnvironmentVariable("OPENAI_API_KEY"))
.AsChatClient("gpt-4o-mini");
// Note: To use the ChatClientBuilder you need to install the Microsoft.Extensions.AI package
IChatClient chatClient = new ChatClientBuilder(openaiClient)
.UseFunctionInvocation()
.Build();
Console.WriteLine("Asking GPT-4o-mini to call the Echo Tool...");
var response = chatClient.CompleteStreamingAsync(
"Please call the echo tool with the string 'Hello MCP!' and give me the response as-is.",
new() { Tools = mappedTools });
await foreach (var update in response)
{
Console.Write(update);
}
Console.WriteLine();
Console.WriteLine("Chat with GPT-4o-mini complete");
}
catch (Exception ex)
{
Console.WriteLine("Error occurred: " + ex.Message);
}
}
}

View file

@ -0,0 +1,7 @@
{
"profiles": {
"SimpleToolsConsole": {
"commandName": "Project"
}
}
}

View file

@ -84,6 +84,7 @@ public sealed class SseTransport : TransportBase
}
}
/// <inheritdoc/>
public override async Task SendMessageAsync(
IJsonRpcMessage message,
CancellationToken cancellationToken = default)

View file

@ -1,7 +1,7 @@
namespace McpDotNet.Protocol.Types;

using System.Text.Json.Serialization;
namespace McpDotNet.Protocol.Types;
/// <summary>
/// Represents annotations that can be attached to content.
/// </summary>

View file

@ -9,10 +9,12 @@ public class CallToolResponse
/// <summary>
/// The server's response to a tools/call request from the client.
/// </summary>
[System.Text.Json.Serialization.JsonPropertyName("content")]
public List<Content> Content { get; set; } = new();
/// <summary>
/// Whether the tool call was unsuccessful. If true, the call was unsuccessful.
/// </summary>
[System.Text.Json.Serialization.JsonPropertyName("isError")]
public bool IsError { get; set; }
}

View file

@ -1,7 +1,7 @@
namespace McpDotNet.Protocol.Types;

using System.Text.Json.Serialization;
namespace McpDotNet.Protocol.Types;
/// <summary>
/// Represents the capabilities that a client may support.
/// </summary>

View file

@ -31,7 +31,7 @@ public class Content
/// <summary>
/// The MIME type of the image.
/// </summary>
[JsonPropertyName("mime_type")]
[JsonPropertyName("mimeType")]
public string? MimeType { get; set; }
/// <summary>

View file

@ -13,40 +13,48 @@ public class CreateMessageRequestParams
/// <summary>
/// A request to include context from one or more MCP servers (including the caller), to be attached to the prompt. The client MAY ignore this request.
/// </summary>
[System.Text.Json.Serialization.JsonPropertyName("includeContext")]
public ContextInclusion? IncludeContext { get; init; }
/// <summary>
/// The maximum number of tokens to sample, as requested by the server. The client MAY choose to sample fewer tokens than requested.
/// </summary>
[System.Text.Json.Serialization.JsonPropertyName("maxTokens")]
public int MaxTokens { get; init; }
/// <summary>
/// Messages requested by the server to be included in the prompt.
/// </summary>
[System.Text.Json.Serialization.JsonPropertyName("messages")]
public required IReadOnlyList<SamplingMessage> Messages { get; init; }
/// <summary>
/// Optional metadata to pass through to the LLM provider. The format of this metadata is provider-specific.
/// </summary>
[System.Text.Json.Serialization.JsonPropertyName("metadata")]
public object? Metadata { get; init; }
/// <summary>
/// The server's preferences for which model to select. The client MAY ignore these preferences.
/// </summary>
[System.Text.Json.Serialization.JsonPropertyName("modelPreferences")]
public ModelPreferences? ModelPreferences { get; init; }
/// <summary>
/// Optional stop sequences that the server wants to use for sampling.
/// </summary>
[System.Text.Json.Serialization.JsonPropertyName("stopSequences")]
public IReadOnlyList<string>? StopSequences { get; init; }
/// <summary>
/// An optional system prompt the server wants to use for sampling. The client MAY modify or omit this prompt.
/// </summary>
[System.Text.Json.Serialization.JsonPropertyName("systemPrompt")]
public string? SystemPrompt { get; init; }
/// <summary>
/// The temperature to use for sampling, as requested by the server.
/// </summary>
[System.Text.Json.Serialization.JsonPropertyName("temperature")]
public float? Temperature { get; init; }
}

View file

@ -24,7 +24,7 @@ public class CreateMessageResult
/// <summary>
/// The reason why sampling stopped, if known.
/// </summary>
[JsonPropertyName("stop_reason")]
[JsonPropertyName("stopReason")]
public string? StopReason { get; init; }
/// <summary>

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