diff --git a/IdentityServer/v7/Directory.Packages.props b/IdentityServer/v7/Directory.Packages.props index 9ec6873f0..262d43caa 100644 --- a/IdentityServer/v7/Directory.Packages.props +++ b/IdentityServer/v7/Directory.Packages.props @@ -43,8 +43,8 @@ - - + + diff --git a/IdentityServer/v7/McpDemo/McpDemo.Client/Program.cs b/IdentityServer/v7/McpDemo/McpDemo.Client/Program.cs index ac9cd3e43..b5294c5d2 100644 --- a/IdentityServer/v7/McpDemo/McpDemo.Client/Program.cs +++ b/IdentityServer/v7/McpDemo/McpDemo.Client/Program.cs @@ -1,8 +1,8 @@ -using System.Diagnostics; +using System.Diagnostics; using System.Net; using System.Text; using System.Web; -using Microsoft.Extensions.Logging; + using ModelContextProtocol.Authentication; using ModelContextProtocol.Client; using ModelContextProtocol.Protocol; @@ -13,38 +13,25 @@ Console.WriteLine($"Connecting to server at {mcpServerUrl}..."); Console.WriteLine(); -// We can customize a shared HttpClient with a custom handler if desired -var sharedHandler = new SocketsHttpHandler -{ - PooledConnectionLifetime = TimeSpan.FromMinutes(2), - PooledConnectionIdleTimeout = TimeSpan.FromMinutes(1) -}; -var httpClient = new HttpClient(sharedHandler); - -var consoleLoggerFactory = LoggerFactory.Create(builder => -{ - builder.AddConsole(); -}); +var httpClient = new HttpClient(); var transport = new HttpClientTransport(new HttpClientTransportOptions { - Endpoint = new Uri(mcpServerUrl), + Endpoint = new Uri(mcpServerUrl), Name = "Weather MCP Client", OAuth = new ClientOAuthOptions { RedirectUri = new Uri("http://localhost:1179/callback"), - AuthorizationRedirectDelegate = HandleAuthorizationUrlAsync, + AuthorizationCallbackHandler = HandleAuthorizationUrlAsync, DynamicClientRegistration = new DynamicClientRegistrationOptions { ClientName = "ProtectedMcpClient" }, - // Odd that this config is required. I would expect the client to read the supported scopes from the MCP server's - // protected resource metadata and use the scopes listed there in its dynamic client registration request. - Scopes = ["mcp:tools"] + Scopes = ["mcp:tools"], }, -}, httpClient, consoleLoggerFactory); +}, httpClient); -var client = await McpClient.CreateAsync(transport, loggerFactory: consoleLoggerFactory); +var client = await McpClient.CreateAsync(transport); var tools = await client.ListToolsAsync(); if (tools.Count == 0) @@ -74,12 +61,13 @@ /// The redirect URI where the authorization code will be sent. /// The cancellation token. /// The authorization code extracted from the callback, or null if the operation failed. -static async Task HandleAuthorizationUrlAsync(Uri authorizationUrl, Uri redirectUri, CancellationToken cancellationToken) +// static async Task HandleAuthorizationUrlAsync(Uri authorizationUrl, Uri redirectUri, CancellationToken cancellationToken) +static async Task HandleAuthorizationUrlAsync(AuthorizationCallbackContext authContext, CancellationToken cancellationToken) { Console.WriteLine("Starting OAuth authorization flow..."); - Console.WriteLine($"Opening browser to: {authorizationUrl}"); + Console.WriteLine($"Opening browser to: {authContext.AuthorizationUri}"); - var listenerPrefix = redirectUri.GetLeftPart(UriPartial.Authority); + var listenerPrefix = authContext.RedirectUri.GetLeftPart(UriPartial.Authority); if (!listenerPrefix.EndsWith("/")) listenerPrefix += "/"; using var listener = new HttpListener(); @@ -90,11 +78,13 @@ listener.Start(); Console.WriteLine($"Listening for OAuth callback on: {listenerPrefix}"); - OpenBrowser(authorizationUrl); + OpenBrowser(authContext.AuthorizationUri); var context = await listener.GetContextAsync(); var query = HttpUtility.ParseQueryString(context.Request.Url?.Query ?? string.Empty); var code = query["code"]; + var state = query["state"]; + var iss = query["iss"]; var error = query["error"]; string responseHtml = "

Authentication complete

You can close this window now.

"; @@ -117,7 +107,12 @@ } Console.WriteLine("Authorization code received successfully."); - return code; + return new AuthorizationResult + { + Code = code, + State = state, + Iss = iss + }; } catch (Exception ex) { diff --git a/IdentityServer/v7/McpDemo/McpDemo.IdentityServer/Config.cs b/IdentityServer/v7/McpDemo/McpDemo.IdentityServer/Config.cs index 9be841355..4f9cc6e68 100644 --- a/IdentityServer/v7/McpDemo/McpDemo.IdentityServer/Config.cs +++ b/IdentityServer/v7/McpDemo/McpDemo.IdentityServer/Config.cs @@ -12,7 +12,7 @@ public static class Config public static IEnumerable ApiResources => [ - new("https://localhost:7141/", "MCP Server") + new("https://localhost:7141", "MCP Server") { Scopes = { "mcp:tools" } } diff --git a/IdentityServer/v7/McpDemo/McpDemo.IdentityServer/HostingExtensions.cs b/IdentityServer/v7/McpDemo/McpDemo.IdentityServer/HostingExtensions.cs index 093df1eec..08632f22e 100644 --- a/IdentityServer/v7/McpDemo/McpDemo.IdentityServer/HostingExtensions.cs +++ b/IdentityServer/v7/McpDemo/McpDemo.IdentityServer/HostingExtensions.cs @@ -1,10 +1,7 @@ -using System.Globalization; using Duende.IdentityServer; using Duende.IdentityServer.Configuration; -using Duende.IdentityServer.Configuration.Validation.DynamicClientRegistration; -using Duende.IdentityServer.Stores; + using Microsoft.AspNetCore.DataProtection; -using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.IdentityModel.Tokens; namespace McpDemo.IdentityServer; @@ -17,10 +14,10 @@ public static WebApplication ConfigureServices(this WebApplicationBuilder builde builder.Services.AddRazorPages(); var isBuilder = builder.Services.AddIdentityServer(options => - { - // this will add the default dynamic client registration endpoint to the discovery/metadatada documents - options.Discovery.DynamicClientRegistration.RegistrationEndpointMode = RegistrationEndpointMode.Inferred; - }) + { + // this will add the default dynamic client registration endpoint to the discovery/metadatada documents + options.Discovery.DynamicClientRegistration.RegistrationEndpointMode = RegistrationEndpointMode.Inferred; + }) .AddTestUsers(TestUsers.Users) .AddLicenseSummary(); diff --git a/IdentityServer/v7/McpDemo/McpDemo.McpServer/Program.cs b/IdentityServer/v7/McpDemo/McpDemo.McpServer/Program.cs index 55e045381..00a3a3803 100644 --- a/IdentityServer/v7/McpDemo/McpDemo.McpServer/Program.cs +++ b/IdentityServer/v7/McpDemo/McpDemo.McpServer/Program.cs @@ -1,21 +1,24 @@ using System.Net.Http.Headers; + using McpDemo.McpServer.McpTools; + using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.IdentityModel.Tokens; + using ModelContextProtocol.AspNetCore.Authentication; var builder = WebApplication.CreateBuilder(args); builder.AddServiceDefaults(); -var serverUrl = "https://localhost:7141"; -var inMemoryOAuthServerUrl = "https://localhost:5001/"; +var mcpServerUrl = "https://localhost:7141"; +var inMemoryOAuthServerUrl = "https://localhost:5001"; builder.Services.AddAuthentication(options => - { - options.DefaultChallengeScheme = McpAuthenticationDefaults.AuthenticationScheme; - options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; - }) +{ + options.DefaultChallengeScheme = McpAuthenticationDefaults.AuthenticationScheme; + options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; +}) .AddJwtBearer(options => { options.Authority = inMemoryOAuthServerUrl; @@ -23,7 +26,7 @@ options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuerSigningKey = true, - ValidAudience = serverUrl, + ValidAudience = mcpServerUrl, ValidIssuer = inMemoryOAuthServerUrl, NameClaimType = "name", RoleClaimType = "role" @@ -33,9 +36,9 @@ { options.ResourceMetadata = new() { - Resource = serverUrl, + Resource = mcpServerUrl, ResourceDocumentation = "https://docs.example/api/weather", - AuthorizationServers = [inMemoryOAuthServerUrl], + AuthorizationServers = { inMemoryOAuthServerUrl }, ScopesSupported = ["mcp:tools"] }; }); diff --git a/IdentityServer/v8/Directory.Packages.props b/IdentityServer/v8/Directory.Packages.props index 30cf1fab9..1e3dc435b 100644 --- a/IdentityServer/v8/Directory.Packages.props +++ b/IdentityServer/v8/Directory.Packages.props @@ -47,8 +47,8 @@ - - + + @@ -63,4 +63,4 @@ - \ No newline at end of file + diff --git a/IdentityServer/v8/McpDemo/McpDemo.AppHost/Properties/launchSettings.json b/IdentityServer/v8/McpDemo/McpDemo.AppHost/Properties/launchSettings.json index 667942c6d..6e041618a 100644 --- a/IdentityServer/v8/McpDemo/McpDemo.AppHost/Properties/launchSettings.json +++ b/IdentityServer/v8/McpDemo/McpDemo.AppHost/Properties/launchSettings.json @@ -13,19 +13,6 @@ "ASPIRE_DASHBOARD_MCP_ENDPOINT_URL": "https://localhost:23176", "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22124" } - }, - "http": { - "commandName": "Project", - "dotnetRunMessages": true, - "launchBrowser": true, - "applicationUrl": "http://localhost:15291", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development", - "DOTNET_ENVIRONMENT": "Development", - "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19220", - "ASPIRE_DASHBOARD_MCP_ENDPOINT_URL": "http://localhost:18149", - "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20034" - } } } } diff --git a/IdentityServer/v8/McpDemo/McpDemo.Client/Program.cs b/IdentityServer/v8/McpDemo/McpDemo.Client/Program.cs index 7e214c248..b5294c5d2 100644 --- a/IdentityServer/v8/McpDemo/McpDemo.Client/Program.cs +++ b/IdentityServer/v8/McpDemo/McpDemo.Client/Program.cs @@ -2,7 +2,7 @@ using System.Net; using System.Text; using System.Web; -using Microsoft.Extensions.Logging; + using ModelContextProtocol.Authentication; using ModelContextProtocol.Client; using ModelContextProtocol.Protocol; @@ -13,38 +13,25 @@ Console.WriteLine($"Connecting to server at {mcpServerUrl}..."); Console.WriteLine(); -// We can customize a shared HttpClient with a custom handler if desired -var sharedHandler = new SocketsHttpHandler -{ - PooledConnectionLifetime = TimeSpan.FromMinutes(2), - PooledConnectionIdleTimeout = TimeSpan.FromMinutes(1) -}; -var httpClient = new HttpClient(sharedHandler); - -var consoleLoggerFactory = LoggerFactory.Create(builder => -{ - builder.AddConsole(); -}); +var httpClient = new HttpClient(); var transport = new HttpClientTransport(new HttpClientTransportOptions { - Endpoint = new Uri(mcpServerUrl), + Endpoint = new Uri(mcpServerUrl), Name = "Weather MCP Client", OAuth = new ClientOAuthOptions { RedirectUri = new Uri("http://localhost:1179/callback"), - AuthorizationRedirectDelegate = HandleAuthorizationUrlAsync, + AuthorizationCallbackHandler = HandleAuthorizationUrlAsync, DynamicClientRegistration = new DynamicClientRegistrationOptions { ClientName = "ProtectedMcpClient" }, - // Odd that this config is required. I would expect the client to read the supported scopes from the MCP server's - // protected resource metadata and use the scopes listed there in its dynamic client registration request. - Scopes = ["mcp:tools"] + Scopes = ["mcp:tools"], }, -}, httpClient, consoleLoggerFactory); +}, httpClient); -var client = await McpClient.CreateAsync(transport, loggerFactory: consoleLoggerFactory); +var client = await McpClient.CreateAsync(transport); var tools = await client.ListToolsAsync(); if (tools.Count == 0) @@ -74,12 +61,13 @@ /// The redirect URI where the authorization code will be sent. /// The cancellation token. /// The authorization code extracted from the callback, or null if the operation failed. -static async Task HandleAuthorizationUrlAsync(Uri authorizationUrl, Uri redirectUri, CancellationToken cancellationToken) +// static async Task HandleAuthorizationUrlAsync(Uri authorizationUrl, Uri redirectUri, CancellationToken cancellationToken) +static async Task HandleAuthorizationUrlAsync(AuthorizationCallbackContext authContext, CancellationToken cancellationToken) { Console.WriteLine("Starting OAuth authorization flow..."); - Console.WriteLine($"Opening browser to: {authorizationUrl}"); + Console.WriteLine($"Opening browser to: {authContext.AuthorizationUri}"); - var listenerPrefix = redirectUri.GetLeftPart(UriPartial.Authority); + var listenerPrefix = authContext.RedirectUri.GetLeftPart(UriPartial.Authority); if (!listenerPrefix.EndsWith("/")) listenerPrefix += "/"; using var listener = new HttpListener(); @@ -90,11 +78,13 @@ listener.Start(); Console.WriteLine($"Listening for OAuth callback on: {listenerPrefix}"); - OpenBrowser(authorizationUrl); + OpenBrowser(authContext.AuthorizationUri); var context = await listener.GetContextAsync(); var query = HttpUtility.ParseQueryString(context.Request.Url?.Query ?? string.Empty); var code = query["code"]; + var state = query["state"]; + var iss = query["iss"]; var error = query["error"]; string responseHtml = "

Authentication complete

You can close this window now.

"; @@ -117,7 +107,12 @@ } Console.WriteLine("Authorization code received successfully."); - return code; + return new AuthorizationResult + { + Code = code, + State = state, + Iss = iss + }; } catch (Exception ex) { diff --git a/IdentityServer/v8/McpDemo/McpDemo.IdentityServer/Config.cs b/IdentityServer/v8/McpDemo/McpDemo.IdentityServer/Config.cs index 9be841355..4f9cc6e68 100644 --- a/IdentityServer/v8/McpDemo/McpDemo.IdentityServer/Config.cs +++ b/IdentityServer/v8/McpDemo/McpDemo.IdentityServer/Config.cs @@ -12,7 +12,7 @@ public static class Config public static IEnumerable ApiResources => [ - new("https://localhost:7141/", "MCP Server") + new("https://localhost:7141", "MCP Server") { Scopes = { "mcp:tools" } } diff --git a/IdentityServer/v8/McpDemo/McpDemo.IdentityServer/HostingExtensions.cs b/IdentityServer/v8/McpDemo/McpDemo.IdentityServer/HostingExtensions.cs index 093df1eec..6820ce4eb 100644 --- a/IdentityServer/v8/McpDemo/McpDemo.IdentityServer/HostingExtensions.cs +++ b/IdentityServer/v8/McpDemo/McpDemo.IdentityServer/HostingExtensions.cs @@ -1,10 +1,6 @@ -using System.Globalization; using Duende.IdentityServer; using Duende.IdentityServer.Configuration; -using Duende.IdentityServer.Configuration.Validation.DynamicClientRegistration; -using Duende.IdentityServer.Stores; using Microsoft.AspNetCore.DataProtection; -using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.IdentityModel.Tokens; namespace McpDemo.IdentityServer; diff --git a/IdentityServer/v8/McpDemo/McpDemo.IdentityServer/Pages/Index.cshtml b/IdentityServer/v8/McpDemo/McpDemo.IdentityServer/Pages/Index.cshtml index 2a5058d5c..7f43dcd13 100644 --- a/IdentityServer/v8/McpDemo/McpDemo.IdentityServer/Pages/Index.cshtml +++ b/IdentityServer/v8/McpDemo/McpDemo.IdentityServer/Pages/Index.cshtml @@ -33,14 +33,14 @@ - @if (Model.License != null) + @if (Model.License != null && Model.License.IsConfigured) {

License

Serial Number
-
@Model.License.SerialNumber
+
@(Model.License.SerialNumber.HasValue ? Model.License.SerialNumber : "???")
Expiration
-
@Model.License.Expiration!.Value.ToString("F")
+
@((Model.License.Expiration.HasValue) ? Model.License.Expiration.Value.ToString("F") : "???")
} diff --git a/IdentityServer/v8/McpDemo/McpDemo.IdentityServer/Properties/launchSettings.json b/IdentityServer/v8/McpDemo/McpDemo.IdentityServer/Properties/launchSettings.json index f40feb3de..50b02f22f 100644 --- a/IdentityServer/v8/McpDemo/McpDemo.IdentityServer/Properties/launchSettings.json +++ b/IdentityServer/v8/McpDemo/McpDemo.IdentityServer/Properties/launchSettings.json @@ -1,12 +1,14 @@ -{ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", "profiles": { - "SelfHost": { + "https": { "commandName": "Project", - "launchBrowser": true, + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "https://localhost:5001", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" - }, - "applicationUrl": "https://localhost:5001" + } } } } diff --git a/IdentityServer/v8/McpDemo/McpDemo.McpServer/Program.cs b/IdentityServer/v8/McpDemo/McpDemo.McpServer/Program.cs index 472f50d75..52f405b20 100644 --- a/IdentityServer/v8/McpDemo/McpDemo.McpServer/Program.cs +++ b/IdentityServer/v8/McpDemo/McpDemo.McpServer/Program.cs @@ -8,8 +8,8 @@ builder.AddServiceDefaults(); -var serverUrl = "https://localhost:7141"; -var inMemoryOAuthServerUrl = "https://localhost:5001/"; +var mcpServerUrl = "https://localhost:7141"; +var inMemoryOAuthServerUrl = "https://localhost:5001"; builder.Services.AddAuthentication(options => { @@ -23,7 +23,7 @@ options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuerSigningKey = true, - ValidAudience = serverUrl, + ValidAudience = mcpServerUrl, ValidIssuer = inMemoryOAuthServerUrl, NameClaimType = "name", RoleClaimType = "role" @@ -33,7 +33,7 @@ { options.ResourceMetadata = new() { - Resource = serverUrl, + Resource = mcpServerUrl, ResourceDocumentation = "https://docs.example/api/weather", AuthorizationServers = { inMemoryOAuthServerUrl }, ScopesSupported = ["mcp:tools"]