diff --git a/samples/AspNetCoreMcpServer/Program.cs b/samples/AspNetCoreMcpServer/Program.cs index 824cd9997..96f89bffa 100644 --- a/samples/AspNetCoreMcpServer/Program.cs +++ b/samples/AspNetCoreMcpServer/Program.cs @@ -3,12 +3,14 @@ using OpenTelemetry.Trace; using AspNetCoreMcpServer.Tools; using AspNetCoreMcpServer.Resources; +using System.Net.Http.Headers; var builder = WebApplication.CreateBuilder(args); builder.Services.AddMcpServer() .WithHttpTransport() .WithTools() .WithTools() + .WithTools() .WithResources(); builder.Services.AddOpenTelemetry() @@ -21,6 +23,13 @@ .WithLogging() .UseOtlpExporter(); +// Configure HttpClientFactory for weather.gov API +builder.Services.AddHttpClient("WeatherApi", client => +{ + client.BaseAddress = new Uri("https://api.weather.gov"); + client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("weather-tool", "1.0")); +}); + var app = builder.Build(); app.MapMcp(); diff --git a/samples/AspNetCoreMcpServer/Tools/WeatherTools.cs b/samples/AspNetCoreMcpServer/Tools/WeatherTools.cs new file mode 100644 index 000000000..b4e3a7414 --- /dev/null +++ b/samples/AspNetCoreMcpServer/Tools/WeatherTools.cs @@ -0,0 +1,73 @@ +using ModelContextProtocol; +using ModelContextProtocol.Server; +using System.ComponentModel; +using System.Globalization; +using System.Text.Json; + +namespace AspNetCoreMcpServer.Tools; + +[McpServerToolType] +public sealed class WeatherTools +{ + private readonly IHttpClientFactory _httpClientFactory; + + public WeatherTools(IHttpClientFactory httpClientFactory) + { + _httpClientFactory = httpClientFactory; + } + + [McpServerTool, Description("Get weather alerts for a US state.")] + public async Task GetAlerts( + [Description("The US state to get alerts for. Use the 2 letter abbreviation for the state (e.g. NY).")] string state) + { + var client = _httpClientFactory.CreateClient("WeatherApi"); + using var responseStream = await client.GetStreamAsync($"/alerts/active/area/{state}"); + using var jsonDocument = await JsonDocument.ParseAsync(responseStream) + ?? throw new McpException("No JSON returned from alerts endpoint"); + + var alerts = jsonDocument.RootElement.GetProperty("features").EnumerateArray(); + + if (!alerts.Any()) + { + return "No active alerts for this state."; + } + + return string.Join("\n--\n", alerts.Select(alert => + { + JsonElement properties = alert.GetProperty("properties"); + return $""" + Event: {properties.GetProperty("event").GetString()} + Area: {properties.GetProperty("areaDesc").GetString()} + Severity: {properties.GetProperty("severity").GetString()} + Description: {properties.GetProperty("description").GetString()} + Instruction: {properties.GetProperty("instruction").GetString()} + """; + })); + } + + [McpServerTool, Description("Get weather forecast for a location.")] + public async Task GetForecast( + [Description("Latitude of the location.")] double latitude, + [Description("Longitude of the location.")] double longitude) + { + var client = _httpClientFactory.CreateClient("WeatherApi"); + var pointUrl = string.Create(CultureInfo.InvariantCulture, $"/points/{latitude},{longitude}"); + + using var locationResponseStream = await client.GetStreamAsync(pointUrl); + using var locationDocument = await JsonDocument.ParseAsync(locationResponseStream); + var forecastUrl = locationDocument?.RootElement.GetProperty("properties").GetProperty("forecast").GetString() + ?? throw new McpException($"No forecast URL provided by {client.BaseAddress}points/{latitude},{longitude}"); + + using var forecastResponseStream = await client.GetStreamAsync(forecastUrl); + using var forecastDocument = await JsonDocument.ParseAsync(forecastResponseStream); + var periods = forecastDocument?.RootElement.GetProperty("properties").GetProperty("periods").EnumerateArray() + ?? throw new McpException("No JSON returned from forecast endpoint"); + + return string.Join("\n---\n", periods.Select(period => $""" + {period.GetProperty("name").GetString()} + Temperature: {period.GetProperty("temperature").GetInt32()}°F + Wind: {period.GetProperty("windSpeed").GetString()} {period.GetProperty("windDirection").GetString()} + Forecast: {period.GetProperty("detailedForecast").GetString()} + """)); + } +} diff --git a/samples/QuickstartClient/Program.cs b/samples/QuickstartClient/Program.cs index 423af627f..d5b887ff8 100644 --- a/samples/QuickstartClient/Program.cs +++ b/samples/QuickstartClient/Program.cs @@ -5,6 +5,7 @@ using ModelContextProtocol.Client; using System.Diagnostics; using System.Runtime.CompilerServices; +using System.Text; var builder = Host.CreateApplicationBuilder(args); @@ -12,16 +13,27 @@ .AddEnvironmentVariables() .AddUserSecrets(); +IClientTransport clientTransport; var (command, arguments) = GetCommandAndArguments(args); -var clientTransport = new StdioClientTransport(new() +if (command == "http") { - Name = "Demo Server", - Command = command, - Arguments = arguments, -}); - -await using var mcpClient = await McpClientFactory.CreateAsync(clientTransport); + // make sure AspNetCoreMcpServer is running + clientTransport = new SseClientTransport(new() + { + Endpoint = new Uri("http://localhost:3001") + }); +} +else +{ + clientTransport = new StdioClientTransport(new() + { + Name = "Demo Server", + Command = command, + Arguments = arguments, + }); +} +await using var mcpClient = await McpClientFactory.CreateAsync(clientTransport!); var tools = await mcpClient.ListToolsAsync(); foreach (var tool in tools) @@ -46,6 +58,9 @@ Console.WriteLine("MCP Client Started!"); Console.ResetColor(); +var messages = new List(); +var sb = new StringBuilder(); + PromptForInput(); while(Console.ReadLine() is string query && !"exit".Equals(query, StringComparison.OrdinalIgnoreCase)) { @@ -55,11 +70,17 @@ continue; } - await foreach (var message in anthropicClient.GetStreamingResponseAsync(query, options)) + messages.Add(new ChatMessage(ChatRole.User, query)); + await foreach (var message in anthropicClient.GetStreamingResponseAsync(messages, options)) { Console.Write(message); + sb.Append(message.ToString()); } + Console.WriteLine(); + sb.AppendLine(); + messages.Add(new ChatMessage(ChatRole.Assistant, sb.ToString())); + sb.Clear(); PromptForInput(); } @@ -79,15 +100,16 @@ static void PromptForInput() /// /// This method uses the file extension of the first argument to determine the command, if it's py, it'll run python, /// if it's js, it'll run node, if it's a directory or a csproj file, it'll run dotnet. -/// +/// /// If no arguments are provided, it defaults to running the QuickstartWeatherServer project from the current repo. -/// +/// /// This method would only be required if you're creating a generic client, such as we use for the quickstart. /// static (string command, string[] arguments) GetCommandAndArguments(string[] args) { return args switch { + [var mode] when mode.Equals("http", StringComparison.OrdinalIgnoreCase) => ("http", args), [var script] when script.EndsWith(".py") => ("python", args), [var script] when script.EndsWith(".js") => ("node", args), [var script] when Directory.Exists(script) || (File.Exists(script) && script.EndsWith(".csproj")) => ("dotnet", ["run", "--project", script]), diff --git a/samples/QuickstartWeatherServer/Program.cs b/samples/QuickstartWeatherServer/Program.cs index 4e6216ee4..9bc050b54 100644 --- a/samples/QuickstartWeatherServer/Program.cs +++ b/samples/QuickstartWeatherServer/Program.cs @@ -15,11 +15,8 @@ options.LogToStandardErrorThreshold = LogLevel.Trace; }); -builder.Services.AddSingleton(_ => -{ - var client = new HttpClient { BaseAddress = new Uri("https://api.weather.gov") }; - client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("weather-tool", "1.0")); - return client; -}); +using var httpClient = new HttpClient { BaseAddress = new Uri("https://api.weather.gov") }; +httpClient.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("weather-tool", "1.0")); +builder.Services.AddSingleton(httpClient); await builder.Build().RunAsync();