Skip to content

Commit 0cf449e

Browse files
authored
Improved samples: added weather tool in AspNetCoreSseServer and option to connect via http to AspNetCoreSseServer. (#469)
1 parent f9129f5 commit 0cf449e

File tree

4 files changed

+117
-16
lines changed

4 files changed

+117
-16
lines changed

samples/AspNetCoreMcpServer/Program.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,14 @@
33
using OpenTelemetry.Trace;
44
using AspNetCoreMcpServer.Tools;
55
using AspNetCoreMcpServer.Resources;
6+
using System.Net.Http.Headers;
67

78
var builder = WebApplication.CreateBuilder(args);
89
builder.Services.AddMcpServer()
910
.WithHttpTransport()
1011
.WithTools<EchoTool>()
1112
.WithTools<SampleLlmTool>()
13+
.WithTools<WeatherTools>()
1214
.WithResources<SimpleResourceType>();
1315

1416
builder.Services.AddOpenTelemetry()
@@ -21,6 +23,13 @@
2123
.WithLogging()
2224
.UseOtlpExporter();
2325

26+
// Configure HttpClientFactory for weather.gov API
27+
builder.Services.AddHttpClient("WeatherApi", client =>
28+
{
29+
client.BaseAddress = new Uri("https://api.weather.gov");
30+
client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("weather-tool", "1.0"));
31+
});
32+
2433
var app = builder.Build();
2534

2635
app.MapMcp();
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
using ModelContextProtocol;
2+
using ModelContextProtocol.Server;
3+
using System.ComponentModel;
4+
using System.Globalization;
5+
using System.Text.Json;
6+
7+
namespace AspNetCoreMcpServer.Tools;
8+
9+
[McpServerToolType]
10+
public sealed class WeatherTools
11+
{
12+
private readonly IHttpClientFactory _httpClientFactory;
13+
14+
public WeatherTools(IHttpClientFactory httpClientFactory)
15+
{
16+
_httpClientFactory = httpClientFactory;
17+
}
18+
19+
[McpServerTool, Description("Get weather alerts for a US state.")]
20+
public async Task<string> GetAlerts(
21+
[Description("The US state to get alerts for. Use the 2 letter abbreviation for the state (e.g. NY).")] string state)
22+
{
23+
var client = _httpClientFactory.CreateClient("WeatherApi");
24+
using var responseStream = await client.GetStreamAsync($"/alerts/active/area/{state}");
25+
using var jsonDocument = await JsonDocument.ParseAsync(responseStream)
26+
?? throw new McpException("No JSON returned from alerts endpoint");
27+
28+
var alerts = jsonDocument.RootElement.GetProperty("features").EnumerateArray();
29+
30+
if (!alerts.Any())
31+
{
32+
return "No active alerts for this state.";
33+
}
34+
35+
return string.Join("\n--\n", alerts.Select(alert =>
36+
{
37+
JsonElement properties = alert.GetProperty("properties");
38+
return $"""
39+
Event: {properties.GetProperty("event").GetString()}
40+
Area: {properties.GetProperty("areaDesc").GetString()}
41+
Severity: {properties.GetProperty("severity").GetString()}
42+
Description: {properties.GetProperty("description").GetString()}
43+
Instruction: {properties.GetProperty("instruction").GetString()}
44+
""";
45+
}));
46+
}
47+
48+
[McpServerTool, Description("Get weather forecast for a location.")]
49+
public async Task<string> GetForecast(
50+
[Description("Latitude of the location.")] double latitude,
51+
[Description("Longitude of the location.")] double longitude)
52+
{
53+
var client = _httpClientFactory.CreateClient("WeatherApi");
54+
var pointUrl = string.Create(CultureInfo.InvariantCulture, $"/points/{latitude},{longitude}");
55+
56+
using var locationResponseStream = await client.GetStreamAsync(pointUrl);
57+
using var locationDocument = await JsonDocument.ParseAsync(locationResponseStream);
58+
var forecastUrl = locationDocument?.RootElement.GetProperty("properties").GetProperty("forecast").GetString()
59+
?? throw new McpException($"No forecast URL provided by {client.BaseAddress}points/{latitude},{longitude}");
60+
61+
using var forecastResponseStream = await client.GetStreamAsync(forecastUrl);
62+
using var forecastDocument = await JsonDocument.ParseAsync(forecastResponseStream);
63+
var periods = forecastDocument?.RootElement.GetProperty("properties").GetProperty("periods").EnumerateArray()
64+
?? throw new McpException("No JSON returned from forecast endpoint");
65+
66+
return string.Join("\n---\n", periods.Select(period => $"""
67+
{period.GetProperty("name").GetString()}
68+
Temperature: {period.GetProperty("temperature").GetInt32()}°F
69+
Wind: {period.GetProperty("windSpeed").GetString()} {period.GetProperty("windDirection").GetString()}
70+
Forecast: {period.GetProperty("detailedForecast").GetString()}
71+
"""));
72+
}
73+
}

samples/QuickstartClient/Program.cs

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,23 +5,35 @@
55
using ModelContextProtocol.Client;
66
using System.Diagnostics;
77
using System.Runtime.CompilerServices;
8+
using System.Text;
89

910
var builder = Host.CreateApplicationBuilder(args);
1011

1112
builder.Configuration
1213
.AddEnvironmentVariables()
1314
.AddUserSecrets<Program>();
1415

16+
IClientTransport clientTransport;
1517
var (command, arguments) = GetCommandAndArguments(args);
1618

17-
var clientTransport = new StdioClientTransport(new()
19+
if (command == "http")
1820
{
19-
Name = "Demo Server",
20-
Command = command,
21-
Arguments = arguments,
22-
});
23-
24-
await using var mcpClient = await McpClientFactory.CreateAsync(clientTransport);
21+
// make sure AspNetCoreMcpServer is running
22+
clientTransport = new SseClientTransport(new()
23+
{
24+
Endpoint = new Uri("http://localhost:3001")
25+
});
26+
}
27+
else
28+
{
29+
clientTransport = new StdioClientTransport(new()
30+
{
31+
Name = "Demo Server",
32+
Command = command,
33+
Arguments = arguments,
34+
});
35+
}
36+
await using var mcpClient = await McpClientFactory.CreateAsync(clientTransport!);
2537

2638
var tools = await mcpClient.ListToolsAsync();
2739
foreach (var tool in tools)
@@ -46,6 +58,9 @@
4658
Console.WriteLine("MCP Client Started!");
4759
Console.ResetColor();
4860

61+
var messages = new List<ChatMessage>();
62+
var sb = new StringBuilder();
63+
4964
PromptForInput();
5065
while(Console.ReadLine() is string query && !"exit".Equals(query, StringComparison.OrdinalIgnoreCase))
5166
{
@@ -55,11 +70,17 @@
5570
continue;
5671
}
5772

58-
await foreach (var message in anthropicClient.GetStreamingResponseAsync(query, options))
73+
messages.Add(new ChatMessage(ChatRole.User, query));
74+
await foreach (var message in anthropicClient.GetStreamingResponseAsync(messages, options))
5975
{
6076
Console.Write(message);
77+
sb.Append(message.ToString());
6178
}
79+
6280
Console.WriteLine();
81+
sb.AppendLine();
82+
messages.Add(new ChatMessage(ChatRole.Assistant, sb.ToString()));
83+
sb.Clear();
6384

6485
PromptForInput();
6586
}
@@ -79,15 +100,16 @@ static void PromptForInput()
79100
/// <remarks>
80101
/// This method uses the file extension of the first argument to determine the command, if it's py, it'll run python,
81102
/// if it's js, it'll run node, if it's a directory or a csproj file, it'll run dotnet.
82-
///
103+
///
83104
/// If no arguments are provided, it defaults to running the QuickstartWeatherServer project from the current repo.
84-
///
105+
///
85106
/// This method would only be required if you're creating a generic client, such as we use for the quickstart.
86107
/// </remarks>
87108
static (string command, string[] arguments) GetCommandAndArguments(string[] args)
88109
{
89110
return args switch
90111
{
112+
[var mode] when mode.Equals("http", StringComparison.OrdinalIgnoreCase) => ("http", args),
91113
[var script] when script.EndsWith(".py") => ("python", args),
92114
[var script] when script.EndsWith(".js") => ("node", args),
93115
[var script] when Directory.Exists(script) || (File.Exists(script) && script.EndsWith(".csproj")) => ("dotnet", ["run", "--project", script]),

samples/QuickstartWeatherServer/Program.cs

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,8 @@
1515
options.LogToStandardErrorThreshold = LogLevel.Trace;
1616
});
1717

18-
builder.Services.AddSingleton(_ =>
19-
{
20-
var client = new HttpClient { BaseAddress = new Uri("https://api.weather.gov") };
21-
client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("weather-tool", "1.0"));
22-
return client;
23-
});
18+
using var httpClient = new HttpClient { BaseAddress = new Uri("https://api.weather.gov") };
19+
httpClient.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("weather-tool", "1.0"));
20+
builder.Services.AddSingleton(httpClient);
2421

2522
await builder.Build().RunAsync();

0 commit comments

Comments
 (0)