Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Stephen Halter <halter73@gmail.com>
120 lines
4.4 KiB
C#
120 lines
4.4 KiB
C#
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
|
using Microsoft.IdentityModel.Tokens;
|
|
using Microsoft.Net.Http.Headers;
|
|
using ModelContextProtocol.AspNetCore.Authentication;
|
|
using ProtectedMcpServer.Tools;
|
|
using System.Net.Http.Headers;
|
|
using System.Security.Claims;
|
|
|
|
var builder = WebApplication.CreateBuilder(args);
|
|
|
|
var serverUrl = "http://localhost:7071/";
|
|
var inMemoryOAuthServerUrl = "https://localhost:7029";
|
|
var allowedOrigins = builder.Configuration.GetSection("Mcp:AllowedOrigins").Get<string[]>() ?? ["http://localhost:5173"];
|
|
|
|
// This sample runs the MCP server on localhost:7071, and it is intended to be callable from a
|
|
// companion web app running on a different host (localhost:5173), while preventing requests from
|
|
// other origins. This scenario requires enabling CORS and enabling a restrictive CORS policy.
|
|
//
|
|
// If your server is not meant for cross-origin browser access, leave CORS disabled.
|
|
//
|
|
// Only apply a lenient CORS policy if your server is intended to be callable from any browser.
|
|
|
|
builder.Services.AddCors(options =>
|
|
{
|
|
options.AddPolicy("McpBrowserClient", policy =>
|
|
{
|
|
policy.WithOrigins(allowedOrigins)
|
|
.WithMethods("POST")
|
|
.WithHeaders(HeaderNames.ContentType, HeaderNames.Authorization, "MCP-Protocol-Version")
|
|
.WithExposedHeaders(HeaderNames.WWWAuthenticate);
|
|
});
|
|
});
|
|
|
|
builder.Services.AddAuthentication(options =>
|
|
{
|
|
options.DefaultChallengeScheme = McpAuthenticationDefaults.AuthenticationScheme;
|
|
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
|
|
})
|
|
.AddJwtBearer(options =>
|
|
{
|
|
// Configure to validate tokens from our in-memory OAuth server
|
|
options.Authority = inMemoryOAuthServerUrl;
|
|
options.TokenValidationParameters = new TokenValidationParameters
|
|
{
|
|
ValidateIssuer = true,
|
|
ValidateAudience = true,
|
|
ValidateLifetime = true,
|
|
ValidateIssuerSigningKey = true,
|
|
ValidAudience = serverUrl, // Validate that the audience matches the resource metadata as suggested in RFC 8707
|
|
ValidIssuer = inMemoryOAuthServerUrl,
|
|
NameClaimType = "name",
|
|
RoleClaimType = "roles"
|
|
};
|
|
|
|
options.Events = new JwtBearerEvents
|
|
{
|
|
OnTokenValidated = context =>
|
|
{
|
|
var name = context.Principal?.Identity?.Name ?? "unknown";
|
|
var email = context.Principal?.FindFirstValue("preferred_username") ?? "unknown";
|
|
Console.WriteLine($"Token validated for: {name} ({email})");
|
|
return Task.CompletedTask;
|
|
},
|
|
OnAuthenticationFailed = context =>
|
|
{
|
|
Console.WriteLine($"Authentication failed: {context.Exception.Message}");
|
|
return Task.CompletedTask;
|
|
},
|
|
OnChallenge = context =>
|
|
{
|
|
Console.WriteLine($"Challenging client to authenticate with Entra ID");
|
|
return Task.CompletedTask;
|
|
}
|
|
};
|
|
})
|
|
.AddMcp(options =>
|
|
{
|
|
options.ResourceMetadata = new()
|
|
{
|
|
ResourceDocumentation = "https://docs.example.com/api/weather",
|
|
AuthorizationServers = { inMemoryOAuthServerUrl },
|
|
ScopesSupported = ["mcp:tools"],
|
|
};
|
|
});
|
|
|
|
builder.Services.AddAuthorization();
|
|
|
|
builder.Services.AddHttpContextAccessor();
|
|
builder.Services.AddMcpServer()
|
|
.WithTools<WeatherTools>()
|
|
.WithHttpTransport(options =>
|
|
{
|
|
// Stateless mode is recommended for servers that don't need server-to-client
|
|
// requests like sampling or elicitation. It enables horizontal scaling without
|
|
// session affinity and works with clients that don't send Mcp-Session-Id.
|
|
options.Stateless = true;
|
|
});
|
|
|
|
// 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.UseCors();
|
|
app.UseAuthentication();
|
|
app.UseAuthorization();
|
|
|
|
// Use the default MCP policy name that we've configured
|
|
app.MapMcp().RequireAuthorization().RequireCors("McpBrowserClient");
|
|
|
|
Console.WriteLine($"Starting MCP server with authorization at {serverUrl}");
|
|
Console.WriteLine($"Using in-memory OAuth server at {inMemoryOAuthServerUrl}");
|
|
Console.WriteLine($"Protected Resource Metadata URL: {serverUrl}.well-known/oauth-protected-resource");
|
|
Console.WriteLine("Press Ctrl+C to stop the server");
|
|
|
|
app.Run(serverUrl);
|