大家好,我是Edison。
近日被MCP刷屏了,刚好看到张队发了一篇文章提到MCP的官方C# SDK发布了预览版,于是手痒痒尝了一下鲜,写了一个DEMO分享给大家。
MCP是什么鬼?
ModelContextProtocol: 0.1.0-preview.2
using ModelContextProtocol.Server;
using System.ComponentModel;
namespace EDT.McpServer.Tools.ConsoleHost;
[ ]
public static class TimeTool
{
[ ]
public static string GetCurrentTime(string city) =>
$"It is {DateTime.Now.Hour}:{DateTime.Now.Minute} in {city}.";
}
using Microsoft.Extensions.Hosting;
using ModelContextProtocol;
using EDT.McpServer.Tools.ConsoleHost;
try
{
Console.WriteLine("Starting MCP Server...");
var builder = Host.CreateEmptyApplicationBuilder(settings: );
builder.Services
.AddMcpServer()
.WithStdioServerTransport()
.WithToolsFromAssembly();
await builder.Build().RunAsync();
return 0;
}
catch (Exception ex)
{
Console.WriteLine($"Host terminated unexpectedly : {ex.Message}");
return 1;
}
ModelContextProtocol: 0.1.0-preview.2
await using var mcpClient = await McpClientFactory.CreateAsync(new()
{
Id = "time",
Name = "Time MCP Server",
TransportType = TransportTypes.StdIo,
TransportOptions = new()
{
["command"] = @"..\..\..\..\EDT.McpServer\bin\Debug\net8.0\EDT.McpServer.exe"
}
});
var tools = await mcpClient.ListToolsAsync();
foreach (var tool in tools)
{
Console.WriteLine($"{tool.Name} ({tool.Description})");
}
var result = await mcpClient.CallToolAsync(
"GetCurrentTime",
new Dictionary<string, object?>() { ["city"] = "Chengdu" },
CancellationToken.None);
Console.WriteLine(result.Content.First(c => c.Type == "text").Text);
var apiKeyCredential = new ApiKeyCredential(config["LLM:ApiKey"]);
var aiClientOptions = new OpenAIClientOptions();
aiClientOptions.Endpoint = new Uri(config["LLM:EndPoint"]);
var aiClient = new OpenAIClient(apiKeyCredential, aiClientOptions)
.AsChatClient(config["LLM:ModelId"]);
var chatClient = new ChatClientBuilder(aiClient)
.UseFunctionInvocation()
.Build();
IList<ChatMessage> chatHistory =
[
new(ChatRole.System, """
You are a helpful assistant delivering time in one sentence
in a short format, like 'It is 10:08 in Paris, France.'
"""),
];
// Core Part: Get AI Tools from MCP Server
var mcpTools = await mcpClient.ListToolsAsync();
var chatOptions = new ChatOptions()
{
Tools = [..mcpTools]
};
// Prompt the user for a question.
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine($"Assistant> How can I assist you today?");
while (true)
{
// Read the user question.
Console.ForegroundColor = ConsoleColor.White;
Console.Write("User> ");
var question = Console.ReadLine();
// Exit the application if the user didn't type anything.
if (!string.IsOrWhiteSpace(question) && question.ToUpper() == "EXIT")
break;
chatHistory.Add(new ChatMessage(ChatRole.User, question));
Console.ForegroundColor = ConsoleColor.Green;
var response = await chatClient.GetResponseAsync(chatHistory, chatOptions);
var content = response.ToString();
Console.WriteLine($"Assistant> {content}");
chatHistory.Add(new ChatMessage(ChatRole.Assistant, content));
Console.WriteLine();
}
ModelContextProtocol: 0.1.0-preview.2
public static class McpEndpointRouteBuilderExtensions
{
public static IEndpointConventionBuilder MapMcpSse(this IEndpointRouteBuilder endpoints)
{
SseResponseStreamTransport? transport = ;
var loggerFactory = endpoints.ServiceProvider.GetRequiredService<ILoggerFactory>();
var mcpServerOptions = endpoints.ServiceProvider.GetRequiredService<IOptions<McpServerOptions>>();
var routeGroup = endpoints.MapGroup("");
routeGroup.MapGet("/sse", async (HttpResponse response, CancellationToken requestAborted) =>
{
response.Headers.ContentType = "text/event-stream";
response.Headers.CacheControl = "no-cache";
await using var localTransport = transport = new SseResponseStreamTransport(response.Body);
await using var server = McpServerFactory.Create(transport, mcpServerOptions.Value, loggerFactory, endpoints.ServiceProvider);
try
{
var transportTask = transport.RunAsync(cancellationToken: requestAborted);
await server.StartAsync(cancellationToken: requestAborted);
await transportTask;
}
catch (OperationCanceledException) when (requestAborted.IsCancellationRequested)
{
// RequestAborted always triggers when the client disconnects before a complete response body is written,
// but this is how SSE connections are typically closed.
}
});
routeGroup.MapPost("/message", async context =>
{
if (transport is )
{
await Results.BadRequest("Connect to the /sse endpoint before sending messages.").ExecuteAsync(context);
return;
}
var message = await context.Request.ReadFromJsonAsync<IJsonRpcMessage>(McpJsonUtilities.DefaultOptions, context.RequestAborted);
if (message is )
{
await Results.BadRequest("No message in request body.").ExecuteAsync(context);
return;
}
await transport.OnMessageReceivedAsync(message, context.RequestAborted);
context.Response.StatusCode = StatusCodes.Status202Accepted;
await context.Response.WriteAsync("Accepted");
});
return routeGroup;
}
}
using EDT.McpServer.WebHost.Extensions;
using EDT.McpServer.WebHost.Tools;
using ModelContextProtocol;
try
{
Console.WriteLine("Starting MCP Server...");
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddMcpServer().WithToolsFromAssembly();
builder.Services.AddWeatherToolService();
var app = builder.Build();
app.UseHttpsRedirection();
app.MapGet("/", () => "Hello MCP Server!");
app.MapMcpSse();
app.Run();
return 0;
}
catch (Exception ex)
{
Console.WriteLine($"Host terminated unexpectedly : {ex.Message}");
return 1;
}
await using var mcpClient = await McpClientFactory.CreateAsync(new()
{
Id = "time",
Name = "Time MCP Server",
TransportType = TransportTypes.Sse,
Location = "https://localhost:8443/sse"
});