Skip to content

Added AspNetCoreClient sample, Extended the existing AspNetCoreSseServer sample #404

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 3 commits into from
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions ModelContextProtocol.sln
Original file line number Diff line number Diff line change
@@ -12,6 +12,9 @@ EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestServerWithHosting", "samples\TestServerWithHosting\TestServerWithHosting.csproj", "{6499876E-2F76-44A8-B6EB-5B889C6E9B7F}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}"
ProjectSection(SolutionItems) = preProject
..\mcp-playground\mcp-shared\ResponseToUser.cs = ..\mcp-playground\mcp-shared\ResponseToUser.cs
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{2A77AF5C-138A-4EBB-9A13-9205DCD67928}"
EndProject
@@ -56,6 +59,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ModelContextProtocol.AspNet
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ModelContextProtocol.AspNetCore.Tests", "tests\ModelContextProtocol.AspNetCore.Tests\ModelContextProtocol.AspNetCore.Tests.csproj", "{85557BA6-3D29-4C95-A646-2A972B1C2F25}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AspNetCoreClient", "samples\AspNetCoreClient\AspNetCoreClient.csproj", "{4C513515-7D79-4DDD-A0B1-CD064835735B}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -110,6 +115,10 @@ Global
{85557BA6-3D29-4C95-A646-2A972B1C2F25}.Debug|Any CPU.Build.0 = Debug|Any CPU
{85557BA6-3D29-4C95-A646-2A972B1C2F25}.Release|Any CPU.ActiveCfg = Release|Any CPU
{85557BA6-3D29-4C95-A646-2A972B1C2F25}.Release|Any CPU.Build.0 = Release|Any CPU
{4C513515-7D79-4DDD-A0B1-CD064835735B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4C513515-7D79-4DDD-A0B1-CD064835735B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4C513515-7D79-4DDD-A0B1-CD064835735B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4C513515-7D79-4DDD-A0B1-CD064835735B}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -128,6 +137,7 @@ Global
{17B8453F-AB72-99C5-E5EA-D0B065A6AE65} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
{37B6A5E0-9995-497D-8B43-3BC6870CC716} = {A2F1F52A-9107-4BF8-8C3F-2F6670E7D0AD}
{85557BA6-3D29-4C95-A646-2A972B1C2F25} = {2A77AF5C-138A-4EBB-9A13-9205DCD67928}
{4C513515-7D79-4DDD-A0B1-CD064835735B} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {384A3888-751F-4D75-9AE5-587330582D89}
30 changes: 30 additions & 0 deletions samples/AspNetCoreClient/AspNetCoreClient.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<UserSecretsId>52baca43-36fc-46be-bb45-84ae606bffed</UserSecretsId>
</PropertyGroup>

<ItemGroup>
<None Remove="Templates\systemMessage-weather.txt" />
</ItemGroup>

<ItemGroup>
<Content Include="Templates\systemMessage-weather.txt">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup>

<ItemGroup>
<PackageReference Include="OpenAI" VersionOverride="2.2.0-beta.4" />
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" VersionOverride="9.4.3-preview.1.25230.7" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\ModelContextProtocol.AspNetCore\ModelContextProtocol.AspNetCore.csproj" />
<ProjectReference Include="..\..\src\ModelContextProtocol\ModelContextProtocol.csproj" />
</ItemGroup>

</Project>
6 changes: 6 additions & 0 deletions samples/AspNetCoreClient/AspNetCoreClient.http
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
@AspNetCoreClient_HostAddress = http://localhost:5157

GET {{AspNetCoreClient_HostAddress}}/weatherforecast/
Accept: application/json

###
35 changes: 35 additions & 0 deletions samples/AspNetCoreClient/ChatClientProxy.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
using Microsoft.Extensions.AI;
using OpenAI.Chat;

namespace AspNetCoreClient
{
public class ChatClientProxy : IChatClient
{
private readonly IChatClient _chatClient;
public ChatClientProxy(ChatClient chatClient)
{
_chatClient = chatClient.AsIChatClient();
}
public void Dispose()
{
_chatClient.Dispose();
}

public async Task<ChatResponse> GetResponseAsync(IEnumerable<Microsoft.Extensions.AI.ChatMessage> messages, ChatOptions? options = null, CancellationToken cancellationToken = default)
{
return await _chatClient.GetResponseAsync(messages, options, cancellationToken);
}

public object? GetService(Type serviceType, object? serviceKey = null)
{
return _chatClient.GetService(serviceType, serviceKey);
}

public IAsyncEnumerable<ChatResponseUpdate> GetStreamingResponseAsync(IEnumerable<Microsoft.Extensions.AI.ChatMessage> messages, ChatOptions? options = null, CancellationToken cancellationToken = default)
{
return _chatClient.GetStreamingResponseAsync(messages, options, cancellationToken);
}
}
}


119 changes: 119 additions & 0 deletions samples/AspNetCoreClient/Controllers/ChatController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Text;
using System.Text.Json;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using ModelContextProtocol.Client;
using ModelContextProtocol.Protocol.Types;
using OpenAI.Chat;

namespace AspNetCoreClient.Controllers
{

[ApiController]
[Route("[controller]")]
public class ChatController : ControllerBase
{

private static readonly Dictionary<Guid,List<ChatMessage>> _Conversations = new();
private readonly ILogger<ChatController> _logger;
private readonly ChatClient _chatClient;
private readonly IMcpClient _mcpClient;
private readonly ITemplatesProvider _templatesProvider;

public ChatController(ILogger<ChatController> logger, ChatClient chatClient, IMcpClient mcpClient, ITemplatesProvider templatesProvider)
{
_logger = logger;
_chatClient = chatClient;
_mcpClient = mcpClient;
_templatesProvider = templatesProvider;
}




[HttpPost(template:"ask", Name = "Ask")]
public async Task<ResponseToUser> Ask(Question question)
{
var tools = await _mcpClient.ListToolsAsync();
List<ChatMessage>? messages = await GetOrCreateConversation(question.ConversationId);
messages.Add(new UserChatMessage(question.Text));
var co = new ChatCompletionOptions();
foreach (var tool in tools)
{
co.Tools.Add(tool.ToOpenAITool());
}
bool requiresAction;

do
{
requiresAction = false;
ChatCompletion completion = _chatClient.CompleteChat(messages, co);

switch (completion.FinishReason)
{
case ChatFinishReason.Stop:
{
// Add the assistant message to the conversation history.
messages.Add(new AssistantChatMessage(completion));
break;
}

case ChatFinishReason.ToolCalls:
{
// First, add the assistant message with tool calls to the conversation history.
messages.Add(new AssistantChatMessage(completion));

// Then, add a new tool message for each tool call that is resolved.
foreach (ChatToolCall toolCall in completion.ToolCalls)
{
if (tools.Select(t => t.Name).Contains(toolCall.FunctionName, StringComparer.OrdinalIgnoreCase))
{
var toolResult = await _mcpClient.CallToolAsync(toolCall.FunctionName, JsonSerializer.Deserialize <Dictionary<string, object?>> (toolCall.FunctionArguments.ToString()));
messages.Add(new ToolChatMessage(toolCall.Id, toolResult.Content[0].Text));
}
else
{
throw new Exception($"Tool {toolCall.FunctionName} not found");
}
}

requiresAction = true;
break;
}

case ChatFinishReason.Length:
throw new NotImplementedException("Incomplete model output due to MaxTokens parameter or token limit exceeded.");

case ChatFinishReason.ContentFilter:
throw new NotImplementedException("Omitted content due to a content filter flag.");

case ChatFinishReason.FunctionCall:
throw new NotImplementedException("Deprecated in favor of tool calls.");

default:
throw new NotImplementedException(completion.FinishReason.ToString());
}
} while (requiresAction);

return new ResponseToUser { Text = messages.Last().Content[0].Text, ConversationId = question.ConversationId };
}

private async Task<List<ChatMessage>> GetOrCreateConversation(Guid conversationId)
{
_Conversations.TryGetValue(conversationId, out var messages);
if (messages == null)
{
messages = new();
messages.Add(new SystemChatMessage(await _templatesProvider.GetSystemMessage("weather")));

_Conversations.Add(conversationId, messages);
}

return messages;
}
}
}


8 changes: 8 additions & 0 deletions samples/AspNetCoreClient/Controllers/ResponseToUser.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace AspNetCoreClient.Controllers
{
public class ResponseToUser
{
public string Text { get; init; } = "";
public Guid ConversationId { get; init; }
}
}
28 changes: 28 additions & 0 deletions samples/AspNetCoreClient/McpExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using ModelContextProtocol.Client;
using OpenAI.Chat;

namespace AspNetCoreClient
{
public static class McpExtensions
{
public static IList<ChatTool> ToOpenAITools(this IList<McpClientTool> tools)
{
var ret = new List<ChatTool>();
foreach (var tool in tools)
{
ret.Add(tool.ToOpenAITool());
}
return ret;
}

public static ChatTool ToOpenAITool(this McpClientTool tool)
{
return ChatTool.CreateFunctionTool(tool.Name, tool.Description, new BinaryData(tool.JsonSchema));
}



}
}


58 changes: 58 additions & 0 deletions samples/AspNetCoreClient/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
using ModelContextProtocol.Client;
using ModelContextProtocol.Protocol.Transport;
using ModelContextProtocol.Protocol.Types;
using Microsoft.Extensions.AI;
using Microsoft.AspNetCore.DataProtection;
using AspNetCoreClient;
var builder = WebApplication.CreateBuilder(args);

// Add services to the container.

builder.Services.AddControllers();
//This client use AspNetCoreSseServer .. so AspNetCoreSseServer must be runing as well

// THIS IS NEEDED FOR SAMPLING CALLBACKS : START
var modelName = builder.Configuration["model-name"];
ArgumentException.ThrowIfNullOrWhiteSpace(modelName, "model-name not found in configuration");
var openAIApiKey = builder.Configuration["open-ai-api-key"]; //PUT IN SECRET FILE OR ENV VARIABLE
ArgumentException.ThrowIfNullOrWhiteSpace(openAIApiKey, "open-ai-api-key not found in configuration");
var client = new OpenAI.OpenAIClient(openAIApiKey);
var chatClient = client.GetChatClient(modelName);
// ChatClientProxy just for demo purpouses, to intercept call back
var samplingClient =new ChatClientProxy(chatClient);
builder.Services.AddSingleton(chatClient);
//END

var useStreamableHttp = builder.Configuration["UseStreamableHttp"] ?? "true";
var sse = "";
if (useStreamableHttp != "true")
{
sse = "/sse";
}
var transport = new SseClientTransport(new SseClientTransportOptions { Endpoint = new Uri($"{builder.Configuration["mcp-server"]}{sse}"), UseStreamableHttp = useStreamableHttp != "true" ? false : true });
// it's important to register using a factory, so I can access ILoggerFactory and pas it to McpClientFactory.CreateAsync
// if not the apparently the mcpclient will recreate a logging setup for any log (not only the mcp client logs)
// and logs will not go to console but to debug window
builder.Services.AddSingleton((serviceProvider) =>
{
var loggerFactory = serviceProvider.GetRequiredService<ILoggerFactory>();
var mcpClient = McpClientFactory.CreateAsync(transport, new McpClientOptions
{
Capabilities = new ClientCapabilities
{
Sampling = new SamplingCapability() { SamplingHandler = samplingClient.CreateSamplingHandler() }
}
}, loggerFactory).Result;
return mcpClient;
});
builder.Services.AddSingleton<ITemplatesProvider, TemplatesProvider>();
var app = builder.Build();


app.UseHttpsRedirection();

app.UseAuthorization();

app.MapControllers();

app.Run();
23 changes: 23 additions & 0 deletions samples/AspNetCoreClient/Properties/launchSettings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
"applicationUrl": "http://localhost:5157",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
"applicationUrl": "https://localhost:7013;http://localhost:5157",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
8 changes: 8 additions & 0 deletions samples/AspNetCoreClient/Question.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace AspNetCoreClient
{
public class Question
{
public string Text { get; init; } = "";
public Guid ConversationId { get; init; } = Guid.NewGuid();
}
}
12 changes: 12 additions & 0 deletions samples/AspNetCoreClient/Templates/systemMessage-weather.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
You are Emma, an AI assistant focused on assisting users to query for the current and past weather in different part of the cities around the world
----
Before calling a function, MAKE SURE all required parameters have been provided by the user along the conversation history.
----
Reuse information if available, avoiding repetitive queries.
----
NEVER GUESS FUNCTION INPUTS! If a user's request is unclear, request further clarification.
----
ALWAYS reply in english
----
Provide correct information but reply creatively and in a funny way, BUT DO NOT ADD EXTRA NON REQUIRED INFO

15 changes: 15 additions & 0 deletions samples/AspNetCoreClient/TemplatesProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
namespace AspNetCoreClient
{
public interface ITemplatesProvider
{
Task<string> GetSystemMessage(string name);
}
public class TemplatesProvider : ITemplatesProvider
{
public async Task <string> GetSystemMessage(string name)
{
var systemMessage = await File.ReadAllTextAsync($"Templates/systemMessage-{name}.txt");
return systemMessage;
}
}
}
Loading