Skip to content
9 changes: 9 additions & 0 deletions samples/AspNetCoreMcpServer/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<EchoTool>()
.WithTools<SampleLlmTool>()
.WithTools<WeatherTools>()
.WithResources<SimpleResourceType>();

builder.Services.AddOpenTelemetry()
Expand All @@ -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();
Expand Down
73 changes: 73 additions & 0 deletions samples/AspNetCoreMcpServer/Tools/WeatherTools.cs
Original file line number Diff line number Diff line change
@@ -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<string> 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<string> 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()}
"""));
}
}
42 changes: 32 additions & 10 deletions samples/QuickstartClient/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,35 @@
using ModelContextProtocol.Client;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Text;

var builder = Host.CreateApplicationBuilder(args);

builder.Configuration
.AddEnvironmentVariables()
.AddUserSecrets<Program>();

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)
Expand All @@ -46,6 +58,9 @@
Console.WriteLine("MCP Client Started!");
Console.ResetColor();

var messages = new List<ChatMessage>();
var sb = new StringBuilder();

PromptForInput();
while(Console.ReadLine() is string query && !"exit".Equals(query, StringComparison.OrdinalIgnoreCase))
{
Expand All @@ -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();
}
Expand All @@ -79,15 +100,16 @@ static void PromptForInput()
/// <remarks>
/// 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.
/// </remarks>
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]),
Expand Down
9 changes: 3 additions & 6 deletions samples/QuickstartWeatherServer/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Loading