Skip to content

Commit 5d38a5b

Browse files
mikekistlerCopilot
andcommitted
Add WeatherAppServer sample demonstrating MCP Apps extension
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent b14455c commit 5d38a5b

9 files changed

Lines changed: 502 additions & 0 deletions

File tree

ModelContextProtocol.slnx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
<Project Path="samples/QuickstartClient/QuickstartClient.csproj" />
5151
<Project Path="samples/QuickstartWeatherServer/QuickstartWeatherServer.csproj" />
5252
<Project Path="samples/TestServerWithHosting/TestServerWithHosting.csproj" />
53+
<Project Path="samples/WeatherAppServer/WeatherAppServer.csproj" />
5354
</Folder>
5455
<Folder Name="/Solution Items/">
5556
<File Path="Directory.Build.props" />
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
using Microsoft.Extensions.DependencyInjection;
2+
using ModelContextProtocol.AspNetCore;
3+
using ModelContextProtocol.Protocol;
4+
using ModelContextProtocol.Server;
5+
using System.Net.Http.Headers;
6+
7+
var builder = WebApplication.CreateBuilder(args);
8+
9+
builder.Services.AddSingleton(_ =>
10+
{
11+
var client = new HttpClient { BaseAddress = new Uri("https://api.weather.gov") };
12+
client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("WeatherAppServer", "1.0"));
13+
return client;
14+
});
15+
16+
builder.Services
17+
.AddMcpServer(options =>
18+
{
19+
options.ServerInfo = new Implementation { Name = "weather-app-server", Version = "1.0.0" };
20+
options.Capabilities = new ServerCapabilities
21+
{
22+
Tools = new ToolsCapability(),
23+
Resources = new ResourcesCapability(),
24+
};
25+
})
26+
.WithHttpTransport()
27+
.WithTools<WeatherTools>()
28+
.WithResources<WeatherResources>()
29+
.WithMcpApps();
30+
31+
var app = builder.Build();
32+
app.MapMcp("/mcp");
33+
app.Run();

samples/WeatherAppServer/README.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# Weather App Server
2+
3+
An MCP server that demonstrates the **MCP Apps** extension by serving an interactive weather forecast UI alongside weather tools.
4+
5+
## What it shows
6+
7+
- **`[McpAppUi]` attribute** — declaratively associates a UI resource with a tool
8+
- **`WithMcpApps()`** — builder extension that processes `[McpAppUi]` attributes
9+
- **UI resource** — an HTML page served via `McpServerResource` with MIME type `text/html;profile=mcp-app`
10+
- **Structured content** — tool results include `StructuredContent` for the UI to render
11+
12+
## Running
13+
14+
```bash
15+
dotnet run
16+
```
17+
18+
The server starts on `http://localhost:5000` by default. Connect any MCP Apps-capable client to the `/mcp` endpoint.
19+
20+
Then prompt that will cause the LLM to request the use of the "weather_ui" tool.
21+
A general prompt like "What's the weather?" will probably work, but if not you could try explicitly requesting the tool
22+
with something like "@weather_ui". This should load the Weather App UI in an iFrame that you can then interact with
23+
to get the weather forecast for a number of US cities.
24+
25+
![UI of the Weather Forecast MCP App in VS Code](./WeatherUI.png)
26+
27+
## Tools
28+
29+
| Tool | Description |
30+
|------|-------------|
31+
| `weather_ui` | Opens the weather forecast UI |
32+
| `weather_forecast` | Gets a multi-period forecast from the National Weather Service for a US city |
33+
34+
Both tools are linked to the `ui://weather-app/forecast` resource via the `[McpAppUi]` attribute.
35+
36+
## Resources
37+
38+
| URI | Description |
39+
|-----|-------------|
40+
| `ui://weather-app/forecast` | Interactive weather forecast HTML UI |
41+
| `data://weather-app/us-cities` | JSON list of supported US cities |

samples/WeatherAppServer/UsCity.cs

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
using System.Text.Json.Serialization;
2+
3+
[JsonConverter(typeof(JsonStringEnumConverter<UsCity>))]
4+
public enum UsCity
5+
{
6+
[JsonStringEnumMemberName("Albuquerque, NM")] AlbuquerqueNM,
7+
[JsonStringEnumMemberName("Atlanta, GA")] AtlantaGA,
8+
[JsonStringEnumMemberName("Austin, TX")] AustinTX,
9+
[JsonStringEnumMemberName("Boston, MA")] BostonMA,
10+
[JsonStringEnumMemberName("Charlotte, NC")] CharlotteNC,
11+
[JsonStringEnumMemberName("Chicago, IL")] ChicagoIL,
12+
[JsonStringEnumMemberName("Dallas, TX")] DallasTX,
13+
[JsonStringEnumMemberName("Denver, CO")] DenverCO,
14+
[JsonStringEnumMemberName("Houston, TX")] HoustonTX,
15+
[JsonStringEnumMemberName("Indianapolis, IN")] IndianapolisIN,
16+
[JsonStringEnumMemberName("Las Vegas, NV")] LasVegasNV,
17+
[JsonStringEnumMemberName("Los Angeles, CA")] LosAngelesCA,
18+
[JsonStringEnumMemberName("Miami, FL")] MiamiFL,
19+
[JsonStringEnumMemberName("Minneapolis, MN")] MinneapolisMN,
20+
[JsonStringEnumMemberName("Nashville, TN")] NashvilleTN,
21+
[JsonStringEnumMemberName("New York, NY")] NewYorkNY,
22+
[JsonStringEnumMemberName("Orlando, FL")] OrlandoFL,
23+
[JsonStringEnumMemberName("Philadelphia, PA")] PhiladelphiaPA,
24+
[JsonStringEnumMemberName("Phoenix, AZ")] PhoenixAZ,
25+
[JsonStringEnumMemberName("Portland, OR")] PortlandOR,
26+
[JsonStringEnumMemberName("Salt Lake City, UT")] SaltLakeCityUT,
27+
[JsonStringEnumMemberName("San Diego, CA")] SanDiegoCA,
28+
[JsonStringEnumMemberName("San Francisco, CA")] SanFranciscoCA,
29+
[JsonStringEnumMemberName("Seattle, WA")] SeattleWA,
30+
[JsonStringEnumMemberName("Washington, DC")] WashingtonDC,
31+
}
32+
33+
public static class UsCityData
34+
{
35+
public static (double Latitude, double Longitude) GetCoordinates(UsCity city) => city switch
36+
{
37+
UsCity.AlbuquerqueNM => (35.0844, -106.6504),
38+
UsCity.AtlantaGA => (33.7490, -84.3880),
39+
UsCity.AustinTX => (30.2672, -97.7431),
40+
UsCity.BostonMA => (42.3601, -71.0589),
41+
UsCity.CharlotteNC => (35.2271, -80.8431),
42+
UsCity.ChicagoIL => (41.8781, -87.6298),
43+
UsCity.DallasTX => (32.7767, -96.7970),
44+
UsCity.DenverCO => (39.7392, -104.9903),
45+
UsCity.HoustonTX => (29.7604, -95.3698),
46+
UsCity.IndianapolisIN => (39.7684, -86.1581),
47+
UsCity.LasVegasNV => (36.1699, -115.1398),
48+
UsCity.LosAngelesCA => (34.0522, -118.2437),
49+
UsCity.MiamiFL => (25.7617, -80.1918),
50+
UsCity.MinneapolisMN => (44.9778, -93.2650),
51+
UsCity.NashvilleTN => (36.1627, -86.7816),
52+
UsCity.NewYorkNY => (40.7128, -74.0060),
53+
UsCity.OrlandoFL => (28.5383, -81.3792),
54+
UsCity.PhiladelphiaPA => (39.9526, -75.1652),
55+
UsCity.PhoenixAZ => (33.4484, -112.0740),
56+
UsCity.PortlandOR => (45.5152, -122.6784),
57+
UsCity.SaltLakeCityUT => (40.7608, -111.8910),
58+
UsCity.SanDiegoCA => (32.7157, -117.1611),
59+
UsCity.SanFranciscoCA => (37.7749, -122.4194),
60+
UsCity.SeattleWA => (47.6062, -122.3321),
61+
UsCity.WashingtonDC => (38.9072, -77.0369),
62+
_ => throw new ArgumentOutOfRangeException(nameof(city))
63+
};
64+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<Project Sdk="Microsoft.NET.Sdk.Web">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net10.0</TargetFramework>
5+
<Nullable>enable</Nullable>
6+
<ImplicitUsings>enable</ImplicitUsings>
7+
<!-- Suppress experimental MCP Apps warning -->
8+
<NoWarn>$(NoWarn);MCPEXP003</NoWarn>
9+
</PropertyGroup>
10+
11+
<ItemGroup>
12+
<ProjectReference Include="..\..\src\ModelContextProtocol.AspNetCore\ModelContextProtocol.AspNetCore.csproj" />
13+
<ProjectReference Include="..\..\src\ModelContextProtocol.Extensions.Apps\ModelContextProtocol.Extensions.Apps.csproj" />
14+
</ItemGroup>
15+
16+
<ItemGroup>
17+
<Content Include="ui\*.html" CopyToOutputDirectory="PreserveNewest" />
18+
</ItemGroup>
19+
20+
</Project>
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
using ModelContextProtocol.Server;
2+
using System.ComponentModel;
3+
using System.Text.Json;
4+
using System.Text.Json.Serialization;
5+
6+
[McpServerResourceType]
7+
public sealed class WeatherResources
8+
{
9+
private static readonly string UiDir = Path.Combine(AppContext.BaseDirectory, "ui");
10+
11+
[McpServerResource(UriTemplate = "ui://weather-app/forecast", Name = "weather-forecast-ui", MimeType = McpApps.ResourceMimeType)]
12+
[Description("Interactive weather forecast UI with city picker")]
13+
public static string GetWeatherForecastUi() => File.ReadAllText(Path.Combine(UiDir, "weather-forecast.html"));
14+
15+
[McpServerResource(UriTemplate = "data://weather-app/us-cities", Name = "us-cities", MimeType = "application/json")]
16+
[Description("List of supported US cities for weather forecasts")]
17+
public static string GetUsCities()
18+
{
19+
var options = new JsonSerializerOptions { Converters = { new JsonStringEnumConverter<UsCity>() } };
20+
var cities = Enum.GetValues<UsCity>().Select(c => JsonSerializer.Serialize(c, options).Trim('"')).Order().ToList();
21+
return JsonSerializer.Serialize(cities);
22+
}
23+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
using ModelContextProtocol;
2+
using ModelContextProtocol.Protocol;
3+
using ModelContextProtocol.Server;
4+
using System.ComponentModel;
5+
using System.Globalization;
6+
using System.Text.Json;
7+
8+
[McpServerToolType]
9+
public sealed class WeatherTools
10+
{
11+
[McpServerTool(Name = "weather_ui")]
12+
[McpAppUi(ResourceUri = "ui://weather-app/forecast")]
13+
[Description("Display an interactive weather forecast UI with city picker.")]
14+
public static string WeatherUi() => "Showing weather forecast UI.";
15+
16+
[McpServerTool(Name = "weather_forecast")]
17+
[McpAppUi(ResourceUri = "ui://weather-app/forecast")]
18+
[Description("Get weather forecast for a US city. Returns detailed multi-period forecast from the National Weather Service.")]
19+
public static async Task<CallToolResult> WeatherForecast(
20+
HttpClient client,
21+
[Description("US city to get the forecast for")] UsCity cityState)
22+
{
23+
var (latitude, longitude) = UsCityData.GetCoordinates(cityState);
24+
var pointUrl = string.Create(CultureInfo.InvariantCulture, $"/points/{latitude},{longitude}");
25+
26+
using var locationResponse = await client.GetAsync(pointUrl);
27+
locationResponse.EnsureSuccessStatusCode();
28+
using var locationDocument = await JsonDocument.ParseAsync(await locationResponse.Content.ReadAsStreamAsync());
29+
30+
var forecastUrl = locationDocument.RootElement.GetProperty("properties").GetProperty("forecast").GetString()
31+
?? throw new McpException($"No forecast URL provided by weather.gov for {cityState}");
32+
33+
using var forecastResponse = await client.GetAsync(forecastUrl);
34+
forecastResponse.EnsureSuccessStatusCode();
35+
using var forecastDocument = await JsonDocument.ParseAsync(await forecastResponse.Content.ReadAsStreamAsync());
36+
37+
var periods = forecastDocument.RootElement.GetProperty("properties").GetProperty("periods").EnumerateArray().ToList();
38+
39+
var structuredPeriods = periods.Select(period => new
40+
{
41+
name = period.GetProperty("name").GetString(),
42+
temperature = period.GetProperty("temperature").GetInt32(),
43+
temperatureUnit = period.GetProperty("temperatureUnit").GetString(),
44+
windSpeed = period.GetProperty("windSpeed").GetString(),
45+
windDirection = period.GetProperty("windDirection").GetString(),
46+
shortForecast = period.GetProperty("shortForecast").GetString(),
47+
detailedForecast = period.GetProperty("detailedForecast").GetString(),
48+
isDaytime = period.GetProperty("isDaytime").GetBoolean()
49+
}).ToList();
50+
51+
return new CallToolResult
52+
{
53+
Content = [new TextContentBlock { Text = $"Weather forecast for {cityState}." }],
54+
StructuredContent = JsonSerializer.SerializeToElement(new
55+
{
56+
cityState = cityState.ToString(),
57+
latitude,
58+
longitude,
59+
periods = structuredPeriods
60+
})
61+
};
62+
}
63+
}
359 KB
Loading

0 commit comments

Comments
 (0)