Skip to content

Commit 4b3f9f7

Browse files
committed
Cleanup
1 parent e8d9a23 commit 4b3f9f7

File tree

5 files changed

+111
-221
lines changed

5 files changed

+111
-221
lines changed

ModelContextProtocol.sln

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ModelContextProtocol.AspNet
5656
EndProject
5757
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ModelContextProtocol.AspNetCore.Tests", "tests\ModelContextProtocol.AspNetCore.Tests\ModelContextProtocol.AspNetCore.Tests.csproj", "{85557BA6-3D29-4C95-A646-2A972B1C2F25}"
5858
EndProject
59+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SecureWeatherClient", "samples\SecureWeatherClient\SecureWeatherClient.csproj", "{CF41BB82-4E3E-5E86-BCB6-0DF3A1B48CF2}"
60+
EndProject
61+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SecureWeatherServer", "samples\SecureWeatherServer\SecureWeatherServer.csproj", "{80944644-54DC-2AFF-C60E-9885AD81E509}"
62+
EndProject
5963
Global
6064
GlobalSection(SolutionConfigurationPlatforms) = preSolution
6165
Debug|Any CPU = Debug|Any CPU
@@ -110,6 +114,14 @@ Global
110114
{85557BA6-3D29-4C95-A646-2A972B1C2F25}.Debug|Any CPU.Build.0 = Debug|Any CPU
111115
{85557BA6-3D29-4C95-A646-2A972B1C2F25}.Release|Any CPU.ActiveCfg = Release|Any CPU
112116
{85557BA6-3D29-4C95-A646-2A972B1C2F25}.Release|Any CPU.Build.0 = Release|Any CPU
117+
{CF41BB82-4E3E-5E86-BCB6-0DF3A1B48CF2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
118+
{CF41BB82-4E3E-5E86-BCB6-0DF3A1B48CF2}.Debug|Any CPU.Build.0 = Debug|Any CPU
119+
{CF41BB82-4E3E-5E86-BCB6-0DF3A1B48CF2}.Release|Any CPU.ActiveCfg = Release|Any CPU
120+
{CF41BB82-4E3E-5E86-BCB6-0DF3A1B48CF2}.Release|Any CPU.Build.0 = Release|Any CPU
121+
{80944644-54DC-2AFF-C60E-9885AD81E509}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
122+
{80944644-54DC-2AFF-C60E-9885AD81E509}.Debug|Any CPU.Build.0 = Debug|Any CPU
123+
{80944644-54DC-2AFF-C60E-9885AD81E509}.Release|Any CPU.ActiveCfg = Release|Any CPU
124+
{80944644-54DC-2AFF-C60E-9885AD81E509}.Release|Any CPU.Build.0 = Release|Any CPU
113125
EndGlobalSection
114126
GlobalSection(SolutionProperties) = preSolution
115127
HideSolutionNode = FALSE
@@ -128,6 +140,8 @@ Global
128140
{17B8453F-AB72-99C5-E5EA-D0B065A6AE65} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
129141
{37B6A5E0-9995-497D-8B43-3BC6870CC716} = {A2F1F52A-9107-4BF8-8C3F-2F6670E7D0AD}
130142
{85557BA6-3D29-4C95-A646-2A972B1C2F25} = {2A77AF5C-138A-4EBB-9A13-9205DCD67928}
143+
{CF41BB82-4E3E-5E86-BCB6-0DF3A1B48CF2} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
144+
{80944644-54DC-2AFF-C60E-9885AD81E509} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
131145
EndGlobalSection
132146
GlobalSection(ExtensibilityGlobals) = postSolution
133147
SolutionGuid = {384A3888-751F-4D75-9AE5-587330582D89}
Lines changed: 12 additions & 221 deletions
Original file line numberDiff line numberDiff line change
@@ -1,242 +1,33 @@
1-
// filepath: c:\Users\ddelimarsky\source\csharp-sdk-anm\samples\SecureWeatherServer\Program.cs
2-
using Microsoft.AspNetCore.Builder;
3-
using Microsoft.Extensions.DependencyInjection;
4-
using ModelContextProtocol.AspNetCore.Auth;
5-
using ModelContextProtocol.Protocol.Messages;
6-
using ModelContextProtocol.Server;
1+
using SecureWeatherServer.Tools;
2+
using System.Net.Http.Headers;
73

84
var builder = WebApplication.CreateBuilder(args);
95

10-
// Add MCP server with OAuth authorization
116
builder.Services.AddMcpServer()
127
.WithHttpTransport()
13-
.WithOAuthAuthorization(metadata =>
8+
.WithTools<WeatherTools>()
9+
.WithAuthorization(metadata =>
1410
{
15-
// Configure the resource metadata
1611
metadata.AuthorizationServers.Add(new Uri("https://auth.example.com"));
17-
metadata.ScopesSupported.AddRange(new[] { "weather.read", "weather.write" });
12+
metadata.ScopesSupported.AddRange(["weather.read", "weather.write"]);
1813
metadata.ResourceDocumentation = new Uri("https://docs.example.com/api/weather");
1914
});
2015

21-
// Build the app
16+
builder.Services.AddSingleton(_ =>
17+
{
18+
var client = new HttpClient() { BaseAddress = new Uri("https://api.weather.gov") };
19+
client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("weather-tool", "1.0"));
20+
return client;
21+
});
22+
2223
var app = builder.Build();
2324

24-
// Enable CORS for development
2525
app.UseCors(policy => policy
2626
.AllowAnyOrigin()
2727
.AllowAnyMethod()
2828
.AllowAnyHeader());
2929

30-
// Configure the HTTP request pipeline
3130
app.UseAuthentication();
3231
app.UseAuthorization();
3332

34-
// Map MCP endpoints with authorization
35-
app.MapMcpWithAuthorization();
36-
37-
// Define weather tool
38-
var weatherTool = new McpTool("get_weather", "Get the current weather for a location")
39-
.WithParameter("location", "The location to get the weather for", typeof(string), required: true);
40-
41-
// Define weather server logic
42-
app.UseMiddleware<ServerInvokeMiddleware>(options =>
43-
{
44-
options.RegisterTool(weatherTool, async (McpToolInvokeParameters parameters, CancellationToken ct) =>
45-
{
46-
if (!parameters.TryGetParameterValue<string>("location", out var location))
47-
{
48-
return McpToolResult.Error("Location parameter is required");
49-
}
50-
51-
// In a real implementation, you would get the weather for the location
52-
// For this example, we'll just return a random weather
53-
var weather = GetRandomWeather(location);
54-
55-
return McpToolResult.Success(new
56-
{
57-
location,
58-
temperature = weather.Temperature,
59-
conditions = weather.Conditions,
60-
humidity = weather.Humidity,
61-
windSpeed = weather.WindSpeed
62-
});
63-
});
64-
});
65-
66-
// Run the app
6733
app.Run();
68-
69-
// Helper method to generate random weather
70-
(double Temperature, string Conditions, int Humidity, double WindSpeed) GetRandomWeather(string location)
71-
{
72-
var random = new Random();
73-
var conditions = new[] { "Sunny", "Cloudy", "Rainy", "Snowy", "Foggy", "Windy" };
74-
75-
return (
76-
Temperature: Math.Round(random.NextDouble() * 40 - 10, 1), // -10 to 30 degrees
77-
Conditions: conditions[random.Next(conditions.Length)],
78-
Humidity: random.Next(30, 95),
79-
WindSpeed: Math.Round(random.NextDouble() * 30, 1)
80-
);
81-
}
82-
83-
// Middleware to handle server invocations
84-
public class ServerInvokeMiddleware
85-
{
86-
private readonly RequestDelegate _next;
87-
private readonly McpServerInvokeOptions _options;
88-
89-
public ServerInvokeMiddleware(RequestDelegate next, McpServerInvokeOptions options)
90-
{
91-
_next = next;
92-
_options = options;
93-
}
94-
95-
public ServerInvokeMiddleware(RequestDelegate next, Action<McpServerInvokeOptions> configureOptions)
96-
: this(next, new McpServerInvokeOptions(configureOptions))
97-
{
98-
}
99-
100-
public async Task InvokeAsync(HttpContext context)
101-
{
102-
// Set up the MCP server with the registered tools
103-
if (context.Features.Get<IMcpServer>() is McpServer server)
104-
{
105-
foreach (var registration in _options.ToolRegistrations)
106-
{
107-
server.RegisterToolHandler(registration.Tool.Definition, registration.Handler);
108-
}
109-
}
110-
111-
await _next(context);
112-
}
113-
}
114-
115-
// Helper classes for tool registration
116-
public class McpServerInvokeOptions
117-
{
118-
public List<ToolRegistration> ToolRegistrations { get; } = new();
119-
120-
public McpServerInvokeOptions() { }
121-
122-
public McpServerInvokeOptions(Action<McpServerInvokeOptions> configure)
123-
{
124-
configure(this);
125-
}
126-
127-
public void RegisterTool(McpTool tool, Func<McpToolInvokeParameters, CancellationToken, Task<McpToolResult>> handler)
128-
{
129-
ToolRegistrations.Add(new ToolRegistration(tool, handler));
130-
}
131-
}
132-
133-
public class ToolRegistration
134-
{
135-
public McpTool Tool { get; }
136-
public Func<McpToolInvokeParameters, CancellationToken, Task<McpToolResult>> Handler { get; }
137-
138-
public ToolRegistration(
139-
McpTool tool,
140-
Func<McpToolInvokeParameters, CancellationToken, Task<McpToolResult>> handler)
141-
{
142-
Tool = tool;
143-
Handler = handler;
144-
}
145-
}
146-
147-
// Helper class to simplify tool registration and parameter handling
148-
public class McpTool
149-
{
150-
public ToolDefinition Definition { get; }
151-
152-
public McpTool(string name, string description)
153-
{
154-
Definition = new ToolDefinition
155-
{
156-
Name = name,
157-
Description = description,
158-
Parameters = new ToolParameterDefinition
159-
{
160-
Properties = {},
161-
Required = new List<string>()
162-
}
163-
};
164-
}
165-
166-
public McpTool WithParameter(string name, string description, Type type, bool required = false)
167-
{
168-
Definition.Parameters.Properties[name] = new ToolPropertyDefinition
169-
{
170-
Description = description,
171-
Type = GetJsonSchemaType(type)
172-
};
173-
174-
if (required)
175-
{
176-
Definition.Parameters.Required.Add(name);
177-
}
178-
179-
return this;
180-
}
181-
182-
private static string GetJsonSchemaType(Type type)
183-
{
184-
return type.Name.ToLowerInvariant() switch
185-
{
186-
"string" => "string",
187-
"int32" or "int64" or "int" or "long" or "double" or "float" or "decimal" => "number",
188-
"boolean" => "boolean",
189-
_ => "object"
190-
};
191-
}
192-
}
193-
194-
// Helper class for the tool invocation parameters
195-
public class McpToolInvokeParameters
196-
{
197-
private readonly Dictionary<string, object?> _parameters;
198-
199-
public McpToolInvokeParameters(Dictionary<string, object?> parameters)
200-
{
201-
_parameters = parameters;
202-
}
203-
204-
public bool TryGetParameterValue<T>(string name, out T value)
205-
{
206-
if (_parameters.TryGetValue(name, out var objValue) && objValue is T typedValue)
207-
{
208-
value = typedValue;
209-
return true;
210-
}
211-
212-
value = default!;
213-
return false;
214-
}
215-
216-
public T? GetParameterValue<T>(string name)
217-
{
218-
if (_parameters.TryGetValue(name, out var value) && value is T typedValue)
219-
{
220-
return typedValue;
221-
}
222-
223-
return default;
224-
}
225-
}
226-
227-
// Helper class for the tool result
228-
public class McpToolResult
229-
{
230-
public object? Result { get; }
231-
public string? Error { get; }
232-
public bool IsError => Error != null;
233-
234-
private McpToolResult(object? result, string? error)
235-
{
236-
Result = result;
237-
Error = error;
238-
}
239-
240-
public static McpToolResult Success(object? result = null) => new McpToolResult(result, null);
241-
public static McpToolResult Error(string error) => new McpToolResult(null, error);
242-
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"profiles": {
3+
"SecureWeatherServer": {
4+
"commandName": "Project",
5+
"launchBrowser": true,
6+
"environmentVariables": {
7+
"ASPNETCORE_ENVIRONMENT": "Development"
8+
},
9+
"applicationUrl": "https://localhost:55598;http://localhost:55599"
10+
}
11+
}
12+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
using System.Text.Json;
2+
3+
namespace ModelContextProtocol;
4+
5+
internal static class HttpClientExt
6+
{
7+
public static async Task<JsonDocument> ReadJsonDocumentAsync(this HttpClient client, string requestUri)
8+
{
9+
using var response = await client.GetAsync(requestUri);
10+
response.EnsureSuccessStatusCode();
11+
return await JsonDocument.ParseAsync(await response.Content.ReadAsStreamAsync());
12+
}
13+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
using ModelContextProtocol;
2+
using ModelContextProtocol.Server;
3+
using System.ComponentModel;
4+
using System.Globalization;
5+
using System.Text.Json;
6+
7+
namespace SecureWeatherServer.Tools;
8+
9+
[McpServerToolType]
10+
public sealed class WeatherTools
11+
{
12+
[McpServerTool, Description("Get weather alerts for a US state.")]
13+
public static async Task<string> GetAlerts(
14+
HttpClient client,
15+
[Description("The US state to get alerts for. Use the 2 letter abbreviation for the state (e.g. NY).")] string state)
16+
{
17+
using var jsonDocument = await client.ReadJsonDocumentAsync($"/alerts/active/area/{state}");
18+
var jsonElement = jsonDocument.RootElement;
19+
var alerts = jsonElement.GetProperty("features").EnumerateArray();
20+
21+
if (!alerts.Any())
22+
{
23+
return "No active alerts for this state.";
24+
}
25+
26+
return string.Join("\n--\n", alerts.Select(alert =>
27+
{
28+
JsonElement properties = alert.GetProperty("properties");
29+
return $"""
30+
Event: {properties.GetProperty("event").GetString()}
31+
Area: {properties.GetProperty("areaDesc").GetString()}
32+
Severity: {properties.GetProperty("severity").GetString()}
33+
Description: {properties.GetProperty("description").GetString()}
34+
Instruction: {properties.GetProperty("instruction").GetString()}
35+
""";
36+
}));
37+
}
38+
39+
[McpServerTool, Description("Get weather forecast for a location.")]
40+
public static async Task<string> GetForecast(
41+
HttpClient client,
42+
[Description("Latitude of the location.")] double latitude,
43+
[Description("Longitude of the location.")] double longitude)
44+
{
45+
var pointUrl = string.Create(CultureInfo.InvariantCulture, $"/points/{latitude},{longitude}");
46+
using var jsonDocument = await client.ReadJsonDocumentAsync(pointUrl);
47+
var forecastUrl = jsonDocument.RootElement.GetProperty("properties").GetProperty("forecast").GetString()
48+
?? throw new Exception($"No forecast URL provided by {client.BaseAddress}points/{latitude},{longitude}");
49+
50+
using var forecastDocument = await client.ReadJsonDocumentAsync(forecastUrl);
51+
var periods = forecastDocument.RootElement.GetProperty("properties").GetProperty("periods").EnumerateArray();
52+
53+
return string.Join("\n---\n", periods.Select(period => $"""
54+
{period.GetProperty("name").GetString()}
55+
Temperature: {period.GetProperty("temperature").GetInt32()}°F
56+
Wind: {period.GetProperty("windSpeed").GetString()} {period.GetProperty("windDirection").GetString()}
57+
Forecast: {period.GetProperty("detailedForecast").GetString()}
58+
"""));
59+
}
60+
}

0 commit comments

Comments
 (0)