HandleAuthorizationUrlAsync(Uri authorizationUrl, Uri redirectUri, CancellationToken cancellationToken)
+{
+ Console.WriteLine("Starting OAuth authorization flow...");
+ Console.WriteLine($"Opening browser to Microsoft Entra ID: {authorizationUrl}");
+
+ var listenerPrefix = redirectUri.GetLeftPart(UriPartial.Authority);
+ if (!listenerPrefix.EndsWith("/")) listenerPrefix += "/";
+
+ using var listener = new HttpListener();
+ listener.Prefixes.Add(listenerPrefix);
+
+ try
+ {
+ listener.Start();
+ Console.WriteLine($"Listening for OAuth callback on: {listenerPrefix}");
+
+ OpenBrowser(authorizationUrl);
+
+ var context = await listener.GetContextAsync();
+ var query = HttpUtility.ParseQueryString(context.Request.Url?.Query ?? string.Empty);
+ var code = query["code"];
+ var error = query["error"];
+ var errorDescription = query["error_description"];
+
+ string responseHtml = "Authentication complete
You can close this window now.
";
+ byte[] buffer = Encoding.UTF8.GetBytes(responseHtml);
+ context.Response.ContentLength64 = buffer.Length;
+ context.Response.ContentType = "text/html";
+ context.Response.OutputStream.Write(buffer, 0, buffer.Length);
+ context.Response.Close();
+
+ if (!string.IsNullOrEmpty(error))
+ {
+ Console.WriteLine($"Auth error: {error}");
+ if (!string.IsNullOrEmpty(errorDescription))
+ {
+ Console.WriteLine($"Error description: {errorDescription}");
+ }
+ return null;
+ }
+
+ if (string.IsNullOrEmpty(code))
+ {
+ Console.WriteLine("No authorization code received");
+ return null;
+ }
+
+ Console.WriteLine("Authorization code received successfully from Microsoft Entra ID.");
+ return code;
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"Error getting auth code: {ex.Message}");
+ return null;
+ }
+ finally
+ {
+ if (listener.IsListening) listener.Stop();
+ }
+}
+
+///
+/// Opens the specified URL in the default browser.
+///
+/// The URL to open.
+static void OpenBrowser(Uri url)
+{
+ // Validate the URI scheme - only allow safe protocols
+ if (url.Scheme != Uri.UriSchemeHttp && url.Scheme != Uri.UriSchemeHttps)
+ {
+ Console.WriteLine($"Error: Only HTTP and HTTPS URLs are allowed.");
+ return;
+ }
+
+ try
+ {
+ var psi = new ProcessStartInfo
+ {
+ FileName = url.ToString(),
+ UseShellExecute = true
+ };
+ Process.Start(psi);
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"Error opening browser: {ex.Message}");
+ Console.WriteLine($"Please manually open this URL: {url}");
+ }
+}
+
+///
+/// Validates the required configuration sections and throws exceptions if any required values are missing.
+///
+/// The MCP client configuration containing server connection details.
+/// The Microsoft Entra ID configuration containing authentication details.
+/// The secured SharePoint site configuration containing site URL.
+/// Thrown when any required configuration value is missing or invalid.
+static void ValidateConfiguration(McpClientConfiguration? mcpConfig, EntraIdConfiguration? entraConfig, SecuredSpoSiteConfiguration? spoConfig)
+{
+ if (mcpConfig?.Url is null)
+ {
+ throw new InvalidOperationException("McpServer:Url configuration is missing.");
+ }
+
+ if (entraConfig is null)
+ {
+ throw new InvalidOperationException("EntraId configuration section is missing.");
+ }
+
+ if (string.IsNullOrEmpty(entraConfig.ClientId))
+ {
+ throw new InvalidOperationException("EntraId:ClientId configuration is missing.");
+ }
+
+ if (string.IsNullOrEmpty(entraConfig.ClientSecret))
+ {
+ throw new InvalidOperationException("EntraId:ClientSecret configuration is required. Consider using user secrets or environment variables for production.");
+ }
+
+ if (string.IsNullOrEmpty(spoConfig?.Url))
+ {
+ throw new InvalidOperationException("SecuredSpoSite:Url configuration is missing.");
+ }
+}
+
+///
+/// Displays startup information to the console, including server URLs and authentication details.
+///
+/// The MCP client configuration containing server connection details.
+/// The Microsoft Entra ID configuration containing authentication details.
+/// The secured SharePoint site configuration containing site URL.
+static void DisplayStartupInfo(McpClientConfiguration mcpConfig, EntraIdConfiguration entraConfig, SecuredSpoSiteConfiguration spoConfig)
+{
+ Console.WriteLine("Protected MCP Client");
+ Console.WriteLine($"Connecting to MCP server at {mcpConfig.Url}...");
+ Console.WriteLine($"Using Microsoft Entra ID tenant: {entraConfig.TenantId}");
+ Console.WriteLine($"Client ID: {entraConfig.ClientId}");
+ Console.WriteLine($"Secured SharePoint Site URL: {spoConfig.Url}");
+ Console.WriteLine("Press Ctrl+C to stop the server");
+}
+
+///
+/// Creates and configures an MCP client with OAuth authentication for secure server communication.
+///
+/// The MCP client configuration containing server connection details.
+/// The Microsoft Entra ID configuration containing OAuth authentication details.
+/// A task that represents the asynchronous operation. The task result contains the configured .
+static async Task CreateMcpClient(McpClientConfiguration mcpConfig, EntraIdConfiguration entraConfig)
+{
+ // 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 transport = new HttpClientTransport(new()
+ {
+ Endpoint = new Uri(mcpConfig.Url),
+ Name = "Secure MCP Client",
+ OAuth = new()
+ {
+ ClientId = entraConfig.ClientId,
+ ClientSecret = entraConfig.ClientSecret,
+ RedirectUri = new Uri(entraConfig.RedirectUri),
+ AuthorizationRedirectDelegate = HandleAuthorizationUrlAsync,
+ Scopes = [
+ $"api://{entraConfig.ServerClientId}/{entraConfig.Scope}"
+ ],
+
+ AdditionalAuthorizationParameters = new Dictionary
+ {
+ ["tenant"] = entraConfig.TenantId,
+ ["response_mode"] = "query"
+ }
+ }
+ }, httpClient, consoleLoggerFactory);
+
+ return await McpClient.CreateAsync(transport, loggerFactory: consoleLoggerFactory);
+}
+
+///
+/// Displays information about the available tools on the MCP server.
+///
+/// The list of tools retrieved from the MCP server.
+static void DisplayTools(IList tools)
+{
+ if (tools.Count == 0)
+ {
+ Console.WriteLine("No tools available on the server.");
+ return;
+ }
+
+ Console.WriteLine($"Found {tools.Count} tools on the server.");
+ Console.WriteLine();
+}
+
+///
+/// Calls the weather alerts tool if available on the server and displays the result.
+///
+/// The list of available tools from the MCP server.
+/// The MCP client instance used to invoke the tool.
+/// A task that represents the asynchronous tool invocation operation.
+static async Task CallWeatherTool(IList tools, McpClient client)
+{
+ if (tools.Any(t => t.Name == "get_alerts"))
+ {
+ Console.WriteLine("Calling get_alerts tool...");
+
+ var result = await client.CallToolAsync(
+ "get_alerts",
+ new Dictionary { { "state", "WA" } }
+ );
+
+ Console.WriteLine("Result: " + ((TextContentBlock)result.Content[0]).Text);
+ Console.WriteLine();
+ }
+}
+
+///
+/// Calls the Microsoft Graph hello tool if available on the server and displays the result.
+///
+/// The list of available tools from the MCP server.
+/// The MCP client instance used to invoke the tool.
+/// A task that represents the asynchronous tool invocation operation.
+static async Task CallGraphTool(IList tools, McpClient client)
+{
+ if (tools.Any(t => t.Name == "hello"))
+ {
+ Console.WriteLine("Calling Hello tool...");
+
+ var result = await client.CallToolAsync("hello");
+
+ Console.WriteLine("Result: " + ((TextContentBlock)result.Content[0]).Text);
+ Console.WriteLine();
+ }
+}
+
+///
+/// Calls the SharePoint site information tool if available on the server and displays the result.
+///
+/// The SharePoint site configuration containing the site URL.
+/// The list of available tools from the MCP server.
+/// The MCP client instance used to invoke the tool.
+/// A task that represents the asynchronous tool invocation operation.
+static async Task CallSpoTool(SecuredSpoSiteConfiguration spoConfig, IList tools, McpClient client)
+{
+ if (tools.Any(t => t.Name == "get_site_info"))
+ {
+ Console.WriteLine("Calling get_site_info tool...");
+ var result = await client.CallToolAsync(
+ "get_site_info",
+ new Dictionary
+ {
+ { "siteUrl", spoConfig.Url }
+ }
+ );
+ Console.WriteLine("Result: " + ((TextContentBlock)result.Content[0]).Text);
+ Console.WriteLine();
+ }
+}
\ No newline at end of file
diff --git a/samples/EntraProtectedMcpClient/README.md b/samples/EntraProtectedMcpClient/README.md
new file mode 100644
index 000000000..0c8eb2ad3
--- /dev/null
+++ b/samples/EntraProtectedMcpClient/README.md
@@ -0,0 +1,194 @@
+# Protected MCP Client Sample with Microsoft Entra ID
+
+This sample demonstrates how to create an MCP client that connects to a protected MCP server using Microsoft Entra ID (Azure AD) OAuth 2.0 authentication. The client implements a custom OAuth authorization flow with browser-based authentication and showcases externalized configuration management.
+
+## Overview
+
+The EntraProtectedMcpClient sample shows how to:
+- Connect to an OAuth-protected MCP server using Microsoft Entra ID
+- Handle OAuth 2.0 authorization code flow with PKCE
+- Use custom authorization redirect handling with local HTTP listener
+- Call protected MCP tools with authentication
+- Manage sensitive data using user secrets
+- Test access to Graph and SharePoint
+
+## Features
+
+- **Microsoft Entra ID Integration**: Full OAuth 2.0 authentication with Microsoft identity platform
+- **User Secrets**: Secure storage of sensitive information during development
+- **Graph and SharePoint Access**: Test authentication through MS Graph and SharePoint
+- **Comprehensive Logging**: Structured logging with configurable levels
+
+## Prerequisites
+
+- .NET 9.0 or later
+- Microsoft Entra ID tenant with registered applications
+- A running EntraProtectedMcpServer (for MCP services)
+- Valid Entra ID credentials configured
+
+## Configuration
+
+### Configuration Files
+
+The client uses a layered configuration approach:
+
+1. **appsettings.json** - Base configuration (non-sensitive)
+2. **appsettings.Development.json** - Development overrides
+3. **User Secrets** - Sensitive data (client secrets, credentials)
+4. **Environment Variables** - Runtime overrides
+
+### Configuration Structure
+
+```json
+{
+ "McpServer": {
+ "Url": "http://localhost:7071/"
+ },
+ "EntraId": {
+ "TenantId": "your-tenant-id",
+ "ClientId": "your-client-id",
+ "ServerClientId": "server-app-registration-id",
+ "RedirectUri": "http://localhost:1179/callback",
+ "Scope": "user_impersonation",
+ "ResponseMode": "query"
+ },
+ "SecuredSpoSite": {
+ "Url": "https://docs.microsoft.com/"
+ }
+}
+```
+
+### Setting Up User Secrets
+
+Store sensitive information securely using .NET user secrets:
+
+```bash
+dotnet user-secrets set "McpClient:EntraId:ClientSecret" "your-client-secret"
+```
+
+### Configuration Classes
+
+The client uses strongly-typed configuration classes:
+
+- **`McpClientConfiguration`** - MCP server connection settings
+- **`EntraIdConfiguration`** - Microsoft Entra ID authentication settings
+- **`SecuredSpoSiteConfiguration`** - SharePoint site configuration for testing
+
+## Setup and Running
+
+### Step 1: Configure Microsoft Entra ID
+
+1. Register two applications in your Entra ID tenant:
+ - **Client Application** (this sample)
+ - **Server Application** (for the MCP server)
+
+2. Configure the client application:
+ - Set redirect URI: `http://localhost:1179/callback`
+ - Enable public client flows if needed
+ - Grant necessary API permissions
+
+3. Configure the server application:
+ - Expose an API with scopes (e.g., `user_impersonation`)
+ - Configure authentication settings
+
+### Step 2: Update Configuration
+
+1. Update `appsettings.json` with your tenant and application IDs
+2. Set the client secret using user secrets (see above)
+3. Optionally configure different SharePoint sites for testing
+
+### Step 3: Start the Protected MCP Server
+
+Start the EntraProtectedMcpServer which provides the protected tools:
+
+The protected server will start at `http://localhost:7071`
+
+### Step 4: Run the Protected MCP Client
+
+Run this client:
+
+```bash
+dotnet run
+```
+
+```plaintext
+Note: Ensure you have the necessary dependencies and runtimes installed.
+```
+
+## What Happens
+
+1. **Configuration Loading**: The client loads configuration from multiple sources (files, user secrets, environment variables)
+2. **Configuration Validation**: Validates required settings and displays helpful error messages
+3. **Server Connection**: Attempts to connect to the protected MCP server at the configured URL
+4. **OAuth Discovery**: The server responds with OAuth metadata indicating authentication is required
+5. **Authentication Flow**: The client initiates Microsoft Entra ID OAuth 2.0 authorization code flow:
+ - Opens a browser to the Entra ID authorization URL
+ - Starts a local HTTP listener on the configured redirect URI
+ - Exchanges the authorization code for an access token using PKCE
+6. **API Access**: The client uses the access token to authenticate with the MCP server
+7. **Tool Execution**: Lists available tools and demonstrates calling various protected tools:
+ - Weather alerts for Washington state
+ - Hello/greeting tools
+ - SharePoint site information retrieval
+
+## Available Tools
+
+Once authenticated, the client can access protected tools including:
+
+- **get_alerts**: Get weather alerts for a US state
+- **get_forecast**: Get weather forecast for a location (latitude/longitude)
+- **hello**: Simple greeting tool for testing using ("https://graph.microsoft.com/v1.0/me")
+- **get_site_info**: Retrieve SharePoint site information (requires appropriate permissions)
+
+## Troubleshooting
+
+### Authentication Issues
+- Ensure the ASP.NET Core dev certificate is trusted:
+- Verify Entra ID application registrations and permissions
+- Check that the client secret is correctly configured in user secrets
+- Ensure the redirect URI matches exactly in both the code and Entra ID app registration
+
+### Configuration Issues
+- Run with debug logging to see configuration loading details
+- Verify all required configuration sections are present
+- Check user secrets are properly configured: `dotnet user-secrets list --project samples/EntraProtectedMcpClient`
+
+### Connection Issues
+- Ensure the EntraProtectedMcpServer is running and accessible
+- Check that ports 7071 and 1179 are available
+- Verify firewall settings allow the required connections
+
+### Browser Issues
+- If the browser doesn't open automatically, copy the authorization URL from the console
+- Allow/trust the OAuth server's certificate in your browser
+- Clear browser cache/cookies if experiencing authentication issues
+
+## Security Considerations
+
+- **Client Secrets**: Never commit client secrets to source control. Always use user secrets for development and secure storage (Key Vault) for production
+- **Redirect URIs**: Ensure redirect URIs are exactly configured in both the application and Entra ID
+- **Scopes**: Request only the minimum necessary permissions/scopes
+- **Token Storage**: The client handles token storage securely in memory only
+
+## Key Files
+
+- **`Program.cs`**: Main client application with OAuth flow implementation and configuration management
+- **`EntraProtectedMcpClient.csproj`**: Project file with dependencies and user secrets configuration
+- **`appsettings.json`**: Base application configuration (non-sensitive)
+- **`Configuration/`**: Strongly-typed configuration classes
+- `McpClientConfiguration.cs`: MCP server settings
+- `EntraIdConfiguration.cs`: Entra ID authentication settings
+- `SecuredSpoSiteConfiguration.cs`: SharePoint site configuration
+
+## Related Samples
+
+- **EntraProtectedMcpServer**: The corresponding server implementation
+- **ProtectedMcpClient**: Alternative client using test OAuth server
+- **ProtectedMcpServer**: Server using test OAuth server
+
+## Additional Resources
+
+- [Microsoft Entra ID Documentation](https://docs.microsoft.com/azure/active-directory/)
+- [OAuth 2.0 Authorization Code Flow](https://docs.microsoft.com/azure/active-directory/develop/v2-oauth2-auth-code-flow)
+- [.NET Configuration Documentation](https://docs.microsoft.com/aspnet/core/fundamentals/configuration/)
+- [User Secrets in Development](https://docs.microsoft.com/aspnet/core/security/app-secrets)
\ No newline at end of file
diff --git a/samples/EntraProtectedMcpClient/appsettings.Development.json b/samples/EntraProtectedMcpClient/appsettings.Development.json
new file mode 100644
index 000000000..5df6729a6
--- /dev/null
+++ b/samples/EntraProtectedMcpClient/appsettings.Development.json
@@ -0,0 +1,8 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Debug",
+ "Microsoft": "Information"
+ }
+ }
+}
\ No newline at end of file
diff --git a/samples/EntraProtectedMcpClient/appsettings.json b/samples/EntraProtectedMcpClient/appsettings.json
new file mode 100644
index 000000000..d6fe56eb3
--- /dev/null
+++ b/samples/EntraProtectedMcpClient/appsettings.json
@@ -0,0 +1,23 @@
+{
+ "McpServer": {
+ "Url": "http://localhost:7071/"
+ },
+ "EntraId": {
+ "TenantId": "your-tenant-id-here",
+ "ClientId": "your-client-id-here",
+ "ServerClientId": "your-server-client-id-here", // The ClientId of the MCP server application
+ "RedirectUri": "http://localhost:1179/callback",
+ "Scope": "user_impersonation", // The scope your MCP server exposes
+ "ResponseMode": "query"
+ },
+ "SecuredSpoSite": {
+ "Url": "https://contoso.sharepoint.com/"
+ },
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft": "Warning",
+ "Microsoft.Hosting.Lifetime": "Information"
+ }
+ }
+}
\ No newline at end of file
diff --git a/samples/EntraProtectedMcpServer/Configuration/EntraIdConfiguration.cs b/samples/EntraProtectedMcpServer/Configuration/EntraIdConfiguration.cs
new file mode 100644
index 000000000..4278584f8
--- /dev/null
+++ b/samples/EntraProtectedMcpServer/Configuration/EntraIdConfiguration.cs
@@ -0,0 +1,15 @@
+namespace EntraProtectedMcpServer.Configuration;
+
+public sealed class EntraIdConfiguration
+{
+ public const string SectionName = "EntraId";
+
+ public required string TenantId { get; set; }
+ public required string ClientId { get; set; }
+ public required string ClientSecret { get; set; }
+ public string? AuthorityUrl { get; set; }
+ public string? TokenEndpoint { get; set; }
+ public List ValidAudiences { get; set; } = [];
+ public List ValidIssuers { get; set; } = [];
+ public List ScopesSupported { get; set; } = [];
+}
\ No newline at end of file
diff --git a/samples/EntraProtectedMcpServer/Configuration/ServerConfiguration.cs b/samples/EntraProtectedMcpServer/Configuration/ServerConfiguration.cs
new file mode 100644
index 000000000..7822d5b46
--- /dev/null
+++ b/samples/EntraProtectedMcpServer/Configuration/ServerConfiguration.cs
@@ -0,0 +1,9 @@
+namespace EntraProtectedMcpServer.Configuration;
+
+public sealed class ServerConfiguration
+{
+ public const string SectionName = "Server";
+
+ public required string Url { get; set; }
+ public string? ResourceDocumentationUrl { get; set; }
+}
\ No newline at end of file
diff --git a/samples/EntraProtectedMcpServer/EntraProtectedMcpServer.csproj b/samples/EntraProtectedMcpServer/EntraProtectedMcpServer.csproj
new file mode 100644
index 000000000..b4c35c779
--- /dev/null
+++ b/samples/EntraProtectedMcpServer/EntraProtectedMcpServer.csproj
@@ -0,0 +1,15 @@
+
+
+
+ net9.0
+ enable
+ enable
+ 783daef3-9c45-408d-a1d3-7caf44724f39
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/samples/EntraProtectedMcpServer/Program.cs b/samples/EntraProtectedMcpServer/Program.cs
new file mode 100644
index 000000000..be7852a42
--- /dev/null
+++ b/samples/EntraProtectedMcpServer/Program.cs
@@ -0,0 +1,187 @@
+using EntraProtectedMcpServer.Configuration;
+using EntraProtectedMcpServer.Tools;
+using Microsoft.AspNetCore.Authentication.JwtBearer;
+using Microsoft.IdentityModel.Tokens;
+using ModelContextProtocol.AspNetCore.Authentication;
+using System.Net.Http.Headers;
+using System.Security.Claims;
+
+var builder = WebApplication.CreateBuilder(args);
+
+// Load configuration from multiple sources
+builder.Configuration
+ .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
+ .AddJsonFile($"appsettings.{builder.Environment.EnvironmentName}.json", optional: true, reloadOnChange: true)
+ .AddEnvironmentVariables()
+ .AddUserSecrets();
+
+// Bind configuration sections to strongly typed objects
+var serverConfig = builder.Configuration.GetSection(ServerConfiguration.SectionName).Get();
+var entraConfig = builder.Configuration.GetSection(EntraIdConfiguration.SectionName).Get();
+
+// Validate required configuration
+ValidateConfiguration(serverConfig, entraConfig);
+
+// Inject IOptions
+builder.Services.Configure(
+ builder.Configuration.GetSection(EntraIdConfiguration.SectionName));
+
+// Build derived configuration values
+var authorityUrl = entraConfig!.AuthorityUrl;
+var validAudiences = entraConfig.ValidAudiences.Count > 0 ? entraConfig.ValidAudiences : [entraConfig.ClientId, $"api://{entraConfig.ClientId}"];
+var validIssuers = entraConfig.ValidIssuers.Count > 0 ? entraConfig.ValidIssuers : [authorityUrl!, $"https://sts.windows.net/{entraConfig.TenantId}/"];
+var scopesSupported = entraConfig.ScopesSupported.Select(scope => $"api://{entraConfig.ClientId}/{scope}").ToList();
+
+// Configure authentication
+ConfigureAuthentication(builder.Services, serverConfig!, entraConfig, authorityUrl!, validAudiences, validIssuers, scopesSupported);
+
+builder.Services.AddAuthorization();
+
+builder.Services.AddHttpContextAccessor();
+builder.Services.AddMcpServer()
+ .WithToolsFromAssembly()
+ .WithHttpTransport();
+
+// 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"));
+});
+
+// Configure HttpClientFactory for Microsoft Graph API
+builder.Services.AddHttpClient("GraphApi", client =>
+{
+ client.BaseAddress = new Uri("https://graph.microsoft.com");
+ client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("graph-tool", "1.0"));
+});
+
+var app = builder.Build();
+
+app.UseAuthentication();
+app.UseAuthorization();
+
+app.MapMcp().RequireAuthorization();
+
+// Display startup information
+DisplayStartupInfo(serverConfig!, entraConfig, authorityUrl!);
+
+app.Run(serverConfig!.Url);
+
+static void ValidateConfiguration(ServerConfiguration? serverConfig, EntraIdConfiguration? entraConfig)
+{
+ if (serverConfig?.Url is null)
+ {
+ throw new InvalidOperationException("Server:Url configuration is required.");
+ }
+
+ if (entraConfig is null)
+ {
+ throw new InvalidOperationException("EntraId configuration section is required.");
+ }
+
+ if (string.IsNullOrEmpty(entraConfig.TenantId))
+ {
+ throw new InvalidOperationException("EntraId:TenantId configuration is required.");
+ }
+
+ if (string.IsNullOrEmpty(entraConfig.ClientId))
+ {
+ throw new InvalidOperationException("EntraId:ClientId configuration is required.");
+ }
+
+ if (string.IsNullOrEmpty(entraConfig.ClientSecret))
+ {
+ throw new InvalidOperationException("EntraId:ClientSecret configuration is required. Consider using user secrets or environment variables for production.");
+ }
+
+ if (string.IsNullOrEmpty(entraConfig.AuthorityUrl))
+ {
+ throw new InvalidOperationException("EntraId:AuthorityUrl configuration is required.");
+ }
+
+ if (string.IsNullOrEmpty(entraConfig.TokenEndpoint))
+ {
+ throw new InvalidOperationException("EntraId:TokenEndpoint configuration is required.");
+ }
+}
+
+static void ConfigureAuthentication(
+ IServiceCollection services,
+ ServerConfiguration serverConfig,
+ EntraIdConfiguration entraConfig,
+ string authorityUrl,
+ IList validAudiences,
+ IList validIssuers,
+ IList scopesSupported)
+{
+ services.AddAuthentication(options =>
+ {
+ options.DefaultChallengeScheme = McpAuthenticationDefaults.AuthenticationScheme;
+ options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
+ })
+ .AddJwtBearer(options =>
+ {
+ options.Authority = authorityUrl;
+ options.TokenValidationParameters = new TokenValidationParameters
+ {
+ ValidateIssuer = true,
+ ValidateAudience = true,
+ ValidateLifetime = true,
+ ValidateIssuerSigningKey = true,
+ ValidAudiences = validAudiences,
+ ValidIssuers = validIssuers,
+ NameClaimType = ClaimTypes.Name,
+ RoleClaimType = ClaimTypes.Role
+ };
+
+ options.Events = new JwtBearerEvents
+ {
+ OnTokenValidated = context =>
+ {
+ var name = context.Principal?.FindFirstValue("name") ??
+ context.Principal?.FindFirstValue("preferred_username") ?? "unknown";
+ var upn = context.Principal?.FindFirstValue("upn") ??
+ context.Principal?.FindFirstValue("email") ??
+ context.Principal?.FindFirstValue("preferred_username") ?? "unknown";
+ var tenantId = context.Principal?.FindFirstValue("tid");
+
+ var logger = context.HttpContext.RequestServices.GetRequiredService>();
+ logger.LogInformation("Token validated for: {Name} ({Upn}) from tenant: {TenantId}", name, upn, tenantId);
+ return Task.CompletedTask;
+ },
+ OnAuthenticationFailed = context =>
+ {
+ var logger = context.HttpContext.RequestServices.GetRequiredService>();
+ logger.LogWarning("Authentication failed: {Message}", context.Exception.Message);
+ return Task.CompletedTask;
+ },
+ OnChallenge = context =>
+ {
+ var logger = context.HttpContext.RequestServices.GetRequiredService>();
+ logger.LogInformation("Challenging client to authenticate with Entra ID");
+ return Task.CompletedTask;
+ }
+ };
+ })
+ .AddMcp(options =>
+ {
+ options.ResourceMetadata = new()
+ {
+ Resource = new Uri(serverConfig.Url),
+ ResourceDocumentation = serverConfig.ResourceDocumentationUrl != null ? new Uri(serverConfig.ResourceDocumentationUrl) : null,
+ AuthorizationServers = { new Uri(entraConfig.AuthorityUrl!) },
+ ScopesSupported = scopesSupported.ToList(),
+ };
+ });
+}
+
+static void DisplayStartupInfo(ServerConfiguration serverConfig, EntraIdConfiguration entraConfig, string authorityUrl)
+{
+ Console.WriteLine($"Starting MCP server with Entra ID authorization at {serverConfig.Url}");
+ Console.WriteLine($"Using Microsoft Entra ID tenant: {entraConfig.TenantId}");
+ Console.WriteLine($"Client ID: {entraConfig.ClientId}");
+ Console.WriteLine($"Authority: {authorityUrl}");
+ Console.WriteLine($"Protected Resource Metadata URL: {serverConfig.Url}.well-known/oauth-protected-resource");
+ Console.WriteLine("Press Ctrl+C to stop the server");
+}
\ No newline at end of file
diff --git a/samples/EntraProtectedMcpServer/Properties/launchSettings.json b/samples/EntraProtectedMcpServer/Properties/launchSettings.json
new file mode 100644
index 000000000..dbc9a1147
--- /dev/null
+++ b/samples/EntraProtectedMcpServer/Properties/launchSettings.json
@@ -0,0 +1,12 @@
+{
+ "profiles": {
+ "ProtectedMcpServer": {
+ "commandName": "Project",
+ "launchBrowser": true,
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ },
+ "applicationUrl": "http://localhost:7071"
+ }
+ }
+}
\ No newline at end of file
diff --git a/samples/EntraProtectedMcpServer/README.md b/samples/EntraProtectedMcpServer/README.md
new file mode 100644
index 000000000..c21be35e3
--- /dev/null
+++ b/samples/EntraProtectedMcpServer/README.md
@@ -0,0 +1,295 @@
+# Protected MCP Server Sample with Microsoft Entra ID
+
+This sample demonstrates how to create an MCP server that requires Microsoft Entra ID (Azure AD) OAuth 2.0
+authentication to access its tools and resources. The server provides weather-related tools, Microsoft Graph
+integration, and SharePoint tools protected by JWT bearer token authentication.
+
+## Overview
+
+The EntraProtectedMcpServer sample shows how to:
+- Create an MCP server with Microsoft Entra ID OAuth 2.0 protection
+- Configure JWT bearer token authentication with Azure AD
+- Implement protected MCP tools and resources
+- Integrate with ASP.NET Core authentication and authorization
+- Provide OAuth resource metadata for client discovery
+- Use On-Behalf-Of (OBO) flow for Microsoft Graph and SharePoint API access
+
+## Features
+
+- **Microsoft Entra ID Integration**: Full OAuth 2.0 authentication with Microsoft identity platform
+- **Weather Tools**: National Weather Service API integration for alerts and forecasts
+- **Microsoft Graph Tools**: Personalized user information and Graph API access
+- **SharePoint Tools**: SharePoint Online REST API integration for site information
+- **On-Behalf-Of Flow**: Secure token exchange for downstream API calls
+- **User Secrets**: Secure storage of sensitive information during development
+
+## Prerequisites
+
+- .NET 9.0 or later
+- Microsoft Entra ID tenant with registered applications
+- Valid Entra ID credentials and API permissions configured
+- Access to Microsoft Graph and SharePoint Online (if using those tools)
+
+## Configuration
+
+### Configuration Files
+
+The server uses a layered configuration approach:
+
+1. **appsettings.json** - Base configuration (non-sensitive)
+2. **appsettings.Development.json** - Development overrides
+3. **User Secrets** - Sensitive data (client secrets, credentials)
+4. **Environment Variables** - Runtime overrides
+
+### Configuration Structure
+
+```json
+{
+ "Server": {
+ "Url": "http://localhost:7071/",
+ "ResourceDocumentationUrl": "https://docs.example.com/api/weather"
+ },
+ "EntraId": {
+ "TenantId": "your-tenant-id",
+ "ClientId": "your-client-id",
+ "ClientSecret": "client-secret-from-user-secrets","
+ "ScopesSupported": [
+ "user_impersonation"
+ ]
+ },
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning",
+ "Microsoft.Hosting.Lifetime": "Information"
+ }
+ },
+ "AllowedHosts": "*"
+}
+```
+
+
+### Configuration Classes
+
+The server uses strongly-typed configuration classes:
+
+- **`ServerConfiguration`** - Server hosting and endpoint settings
+- **`EntraIdConfiguration`** - Microsoft Entra ID authentication settings
+
+## Setup and Running
+
+### Step 1: Configure Microsoft Entra ID
+
+1. Register an application in your Entra ID tenant for the MCP server
+2. Configure API permissions:
+ - **Microsoft Graph**: `User.Read` (for Graph tools)
+ - **SharePoint**: `Sites.Read.All` or appropriate site permissions (for SharePoint tools)
+3. Create a client secret for the On-Behalf-Of flow
+4. Note down the Tenant ID and Client ID
+
+### Step 2: Update Configuration
+
+1. Update `appsettings.json` with your tenant and application IDs
+2. Set the client secret using user secrets (see above)
+3. Optionally configure external API settings
+
+### Step 3: Run the Protected MCP Server
+
+## Running the Server
+
+### Locally with Visual Studio
+
+1. Press F5 to build and run the server
+2. The server will start at `http://localhost:7071`
+
+### Docker
+
+To run the server in a Docker container:
+
+```bash
+docker build -t entra-protected-mcp-server .
+docker run -d -p 7071:80 --name mcp-server entra-protected-mcp-server
+```
+
+## Testing the Server
+
+### With Protected MCP Client
+
+Use the ProtectedMcpClient sample to test the server:
+
+```bash
+cd samples\ProtectedMcpClient
+dotnet run
+```
+
+
+## Testing Without Client
+
+You can test the server directly using HTTP tools:
+
+1. Get an access token from Microsoft Entra ID with appropriate scopes
+2. Include the token in the `Authorization: Bearer ` header
+3. Make requests to the MCP endpoints:
+ - `POST http://localhost:7071/` with MCP JSON-RPC requests
+ - `GET http://localhost:7071/.well-known/oauth-protected-resource` for OAuth metadata
+
+### Example MCP Request
+
+```
+curl -X POST http://localhost:7071/
+-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
+-H "Content-Type: application/json"
+-H "ProtocolVersion: 2025-06-18"
+-d '{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "hello" } }'
+```
+
+## What the Server Provides
+
+### Protected Resources
+
+- **MCP Endpoint**: `http://localhost:7071/` (requires authentication)
+- **OAuth Resource Metadata**: `http://localhost:7071/.well-known/oauth-protected-resource`
+
+### Available Tools
+
+The server provides several categories of protected tools:
+
+#### Weather Tools (WeatherTools.cs)
+1. **GetAlerts**: Get weather alerts for a US state
+ - Parameter: `state` (string) - 2-letter US state abbreviation
+ - Example: `GetAlerts` with `state: "WA"`
+ - Data source: National Weather Service API
+
+2. **GetForecast**: Get weather forecast for a location
+ - Parameters:
+ - `latitude` (double) - Latitude coordinate
+ - `longitude` (double) - Longitude coordinate
+ - Example: `GetForecast` with `latitude: 47.6062, longitude: -122.3321`
+ - Data source: National Weather Service API
+
+#### Microsoft Graph Tools (GraphTools.cs)
+1. **Hello**: Get a personalized greeting using Microsoft Graph
+ - No parameters required
+ - Uses On-Behalf-Of flow to access Microsoft Graph
+ - Retrieves user's display name from `/me` endpoint
+ - Requires: `User.Read` permission
+
+#### SharePoint Tools (SPOTools.cs)
+1. **GetSiteInfo**: Get SharePoint site information
+ - Parameter: `siteUrl` (string) - SharePoint site URL
+ - Example: `GetSiteInfo` with `siteUrl: "https://contoso.sharepoint.com/sites/sitename"`
+ - Uses On-Behalf-Of flow to access SharePoint REST API
+ - Returns site title, description, URL, creation date, and language
+ - Requires: Appropriate SharePoint permissions
+
+### Authentication & Authorization Flow
+
+1. **Initial Authentication**: Client authenticates with Entra ID and receives an access token
+2. **MCP Request**: Client includes the access token in the `Authorization: Bearer` header
+3. **Token Validation**: Server validates the JWT token against Entra ID
+4. **On-Behalf-Of Flow**: For Graph/SharePoint tools, server exchanges the user's token for downstream API tokens
+5. **API Access**: Server uses the exchanged tokens to call Microsoft Graph or SharePoint APIs
+6. **Response**: Server returns the processed results to the client
+
+## Architecture
+
+The server uses:
+- **ASP.NET Core** for hosting and HTTP handling
+- **JWT Bearer Authentication** for Microsoft Entra ID token validation
+- **MCP Authentication Extensions** for OAuth resource metadata
+- **On-Behalf-Of (OBO) Flow** for secure token exchange
+- **HttpClientFactory** for calling external APIs (Weather.gov, Microsoft Graph, SharePoint)
+- **Authorization** to protect MCP endpoints
+- **Strongly-typed Configuration** for settings management
+
+## Configuration Details
+
+### Default Settings
+- **Server URL**: `http://localhost:7071`
+- **Resource Documentation**: `https://docs.example.com/api/weather`
+- **Weather API**: `https://api.weather.gov`
+- **Microsoft Graph API**: `https://graph.microsoft.com`
+
+### Required Permissions
+- **Microsoft Graph**: `User.Read`
+- **SharePoint**: `Sites.Read.All` or site-specific permissions
+- **MCP Server**: `user_impersonation` scope
+
+
+## External Dependencies
+
+- **National Weather Service API** (`api.weather.gov`): Real weather data
+- **Microsoft Graph API** (`graph.microsoft.com`): User information and Microsoft 365 data
+- **SharePoint REST API**: SharePoint Online site and content access
+- **Microsoft Entra ID**: Authentication and token services
+
+## Troubleshooting
+
+### Configuration Issues
+- Verify all required configuration sections are present in `appsettings.json`
+- Check user secrets are properly configured: `dotnet user-secrets list --project samples/EntraProtectedMcpServer`
+- Ensure client secret is set for On-Behalf-Of flow
+
+### Authentication Issues
+- Ensure the ASP.NET Core dev certificate is trusted:
+
+```
+dotnet dev-certs https --clean dotnet dev-certs https --trust
+```
+
+- Verify Entra ID application registrations and permissions
+- Check token audience and issuer validation settings
+- Ensure API permissions are granted and admin consented
+
+### On-Behalf-Of Flow Issues
+- Verify the client application has the necessary delegated permissions
+- Check that the client secret is correctly configured
+- Ensure the original access token has the required scopes
+- Verify the target APIs (Graph/SharePoint) are accessible
+
+### Network Issues
+- Check that port 7071 is available and not blocked by firewall
+- Verify external API endpoints are accessible (weather.gov, graph.microsoft.com)
+- Check proxy settings if behind a corporate firewall
+
+### API Permission Issues
+- Ensure Microsoft Graph permissions are granted: `User.Read`
+- Verify SharePoint permissions are appropriate for the target sites
+- Check that admin consent has been provided for application permissions
+
+## Security Considerations
+
+- **Client Secrets**: Never commit client secrets to source control. Always use user secrets for development and secure storage (Key Vault) for production
+- **Token Scoping**: Request only the minimum necessary permissions/scopes
+- **On-Behalf-Of Flow**: Tokens are exchanged securely and not stored persistently
+- **HTTPS**: Use HTTPS in production environments
+- **Token Validation**: All tokens are properly validated against Microsoft Entra ID
+
+## Key Files
+
+- **`Program.cs`**: Server setup with authentication and MCP configuration
+- **`Configuration/`**: Strongly-typed configuration classes
+- `ServerConfiguration.cs`: Server hosting settings
+- `EntraIdConfiguration.cs`: Entra ID authentication settings
+- `ExternalApiConfiguration.cs`: External API configurations
+- **`Tools/`**: MCP tool implementations
+- `WeatherTools.cs`: Weather-related tools using weather.gov API
+- `GraphTools.cs`: Microsoft Graph integration with On-Behalf-Of flow
+- `SPOTools.cs`: SharePoint Online REST API integration
+- **`appsettings.json`**: Base application configuration
+- **`EntraProtectedMcpServer.csproj`**: Project file with dependencies and user secrets
+
+## Related Samples
+
+- **EntraProtectedMcpClient**: Corresponding client implementation with Entra ID
+- **ProtectedMcpClient**: Alternative client using test OAuth server
+- **ProtectedMcpServer**: Server using test OAuth server instead of Entra ID
+
+## Additional Resources
+
+- [Microsoft Entra ID Documentation](https://docs.microsoft.com/azure/active-directory/)
+- [On-Behalf-Of Flow](https://docs.microsoft.com/azure/active-directory/develop/v2-oauth2-on-behalf-of-flow)
+- [Microsoft Graph API](https://docs.microsoft.com/graph/)
+- [SharePoint REST API](https://docs.microsoft.com/sharepoint/dev/sp-add-ins/get-to-know-the-sharepoint-rest-service)
+- [ASP.NET Core Authentication](https://docs.microsoft.com/aspnet/core/security/authentication/)
+- [.NET Configuration](https://docs.microsoft.com/aspnet/core/fundamentals/configuration/)
\ No newline at end of file
diff --git a/samples/EntraProtectedMcpServer/Tools/GraphTools.cs b/samples/EntraProtectedMcpServer/Tools/GraphTools.cs
new file mode 100644
index 000000000..8265880d2
--- /dev/null
+++ b/samples/EntraProtectedMcpServer/Tools/GraphTools.cs
@@ -0,0 +1,162 @@
+using ModelContextProtocol;
+using ModelContextProtocol.Server;
+using EntraProtectedMcpServer.Configuration;
+using System.ComponentModel;
+using System.Net.Http.Headers;
+using System.Text.Json;
+using Microsoft.Extensions.Options;
+
+namespace EntraProtectedMcpServer.Tools;
+
+[McpServerToolType]
+public sealed class GraphTools
+{
+ private readonly IHttpClientFactory _httpClientFactory;
+ private readonly IHttpContextAccessor _httpContextAccessor;
+ private readonly IConfiguration _configuration;
+ private readonly EntraIdConfiguration _entraIdConfig;
+
+ ///
+ /// Initializes a new instance of the GraphTools class.
+ ///
+ public GraphTools(
+ IHttpClientFactory httpClientFactory,
+ IHttpContextAccessor httpContextAccessor,
+ IConfiguration configuration,
+ IOptions entraIdOptions)
+ {
+ _httpClientFactory = httpClientFactory;
+ _httpContextAccessor = httpContextAccessor;
+ _configuration = configuration;
+ _entraIdConfig = entraIdOptions.Value;
+ }
+
+ ///
+ /// Gets a personalized greeting using Microsoft Graph to retrieve the user's display name.
+ ///
+ /// A personalized hello message with the user's name.
+ [McpServerTool, Description("Get a personalized hello message using Microsoft Graph.")]
+ public async Task Hello()
+ {
+ // Get the current user's access token from the HTTP context
+ var httpContext = _httpContextAccessor.HttpContext
+ ?? throw new McpException("HTTP context not available");
+
+ var authHeader = httpContext.Request.Headers.Authorization.FirstOrDefault();
+ if (string.IsNullOrEmpty(authHeader) || !authHeader.StartsWith("Bearer "))
+ {
+ throw new McpException("No valid bearer token found in request");
+ }
+
+ var mcpAccessToken = authHeader["Bearer ".Length..];
+
+ try
+ {
+ // Exchange the MCP token for a Microsoft Graph token using On-Behalf-Of flow
+ var graphAccessToken = await GetGraphTokenAsync(mcpAccessToken);
+
+ // Create HTTP client for Microsoft Graph
+ var client = _httpClientFactory.CreateClient("GraphApi");
+
+ // Add the Graph access token to the request
+ client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", graphAccessToken);
+
+ // Call Microsoft Graph /me endpoint
+ var response = await client.GetAsync("/v1.0/me");
+
+ if (!response.IsSuccessStatusCode)
+ {
+ throw new McpException($"Microsoft Graph API call failed: {response.StatusCode} - {await response.Content.ReadAsStringAsync()}");
+ }
+
+ var jsonContent = await response.Content.ReadAsStringAsync();
+ using var document = JsonDocument.Parse(jsonContent);
+ var root = document.RootElement;
+
+ // Extract the user's display name from the response
+ var displayName = root.TryGetProperty("displayName", out var nameElement)
+ ? nameElement.GetString()
+ : null;
+
+ // Fallback to other name properties if displayName is not available
+ if (string.IsNullOrEmpty(displayName))
+ {
+ displayName = root.TryGetProperty("givenName", out var givenNameElement)
+ ? givenNameElement.GetString()
+ : root.TryGetProperty("userPrincipalName", out var upnElement)
+ ? upnElement.GetString()?.Split('@')[0] // Take the part before @ for UPN
+ : "User";
+ }
+
+ return $"Hello, {displayName}!!";
+ }
+ catch (HttpRequestException ex)
+ {
+ throw new McpException($"Failed to call Microsoft Graph API: {ex.Message}", ex);
+ }
+ catch (JsonException ex)
+ {
+ throw new McpException($"Failed to parse Microsoft Graph API response: {ex.Message}", ex);
+ }
+ }
+
+ ///
+ /// Exchanges an MCP access token for a Microsoft Graph access token using On-Behalf-Of flow.
+ ///
+ /// The original MCP access token.
+ /// A Microsoft Graph access token.
+ private async Task GetGraphTokenAsync(string mcpAccessToken)
+ {
+ // Validate required configuration
+ if (string.IsNullOrEmpty(_entraIdConfig.TenantId))
+ {
+ throw new McpException("EntraId:TenantId configuration is required for On-Behalf-Of flow");
+ }
+
+ if (string.IsNullOrEmpty(_entraIdConfig.ClientId))
+ {
+ throw new McpException("EntraId:ClientId configuration is required for On-Behalf-Of flow");
+ }
+
+ // Get client secret from configuration (should be in user secrets or secure storage)
+ var clientSecret = _entraIdConfig.ClientSecret ?? _configuration["AzureAd:ClientSecret"];
+ if (string.IsNullOrEmpty(clientSecret))
+ {
+ throw new McpException("Client secret not configured for On-Behalf-Of flow. Check EntraId:ClientSecret or AzureAd:ClientSecret configuration.");
+ }
+
+ var client = _httpClientFactory.CreateClient();
+
+ var tokenEndpoint = _entraIdConfig.TokenEndpoint;
+
+ var requestParams = new Dictionary
+ {
+ ["grant_type"] = "urn:ietf:params:oauth:grant-type:jwt-bearer",
+ ["client_id"] = _entraIdConfig.ClientId,
+ ["client_secret"] = clientSecret,
+ ["assertion"] = mcpAccessToken,
+ ["scope"] = "https://graph.microsoft.com/User.Read",
+ ["requested_token_use"] = "on_behalf_of"
+ };
+
+ var requestContent = new FormUrlEncodedContent(requestParams);
+
+ var response = await client.PostAsync(tokenEndpoint, requestContent);
+
+ if (!response.IsSuccessStatusCode)
+ {
+ var errorContent = await response.Content.ReadAsStringAsync();
+ throw new McpException($"On-Behalf-Of token exchange failed: {response.StatusCode} - {errorContent}");
+ }
+
+ var responseContent = await response.Content.ReadAsStringAsync();
+ using var document = JsonDocument.Parse(responseContent);
+
+ if (!document.RootElement.TryGetProperty("access_token", out var tokenElement))
+ {
+ throw new McpException("No access token in On-Behalf-Of response");
+ }
+
+ return tokenElement.GetString() ?? throw new McpException("Access token is null");
+ }
+}
\ No newline at end of file
diff --git a/samples/EntraProtectedMcpServer/Tools/SPOTools.cs b/samples/EntraProtectedMcpServer/Tools/SPOTools.cs
new file mode 100644
index 000000000..a46634e37
--- /dev/null
+++ b/samples/EntraProtectedMcpServer/Tools/SPOTools.cs
@@ -0,0 +1,149 @@
+using ModelContextProtocol;
+using ModelContextProtocol.Server;
+using EntraProtectedMcpServer.Configuration;
+using System.ComponentModel;
+using System.Net.Http.Headers;
+using System.Text.Json;
+using Microsoft.Extensions.Options;
+
+namespace EntraProtectedMcpServer.Tools;
+
+[McpServerToolType]
+public sealed class SPOTools
+{
+ private readonly IHttpClientFactory _httpClientFactory;
+ private readonly IHttpContextAccessor _httpContextAccessor;
+ private readonly IConfiguration _configuration;
+ private readonly EntraIdConfiguration _entraIdConfig;
+
+ ///
+ /// Initializes a new instance of the SPOTools class.
+ ///
+ public SPOTools(
+ IHttpClientFactory httpClientFactory,
+ IHttpContextAccessor httpContextAccessor,
+ IConfiguration configuration,
+ IOptions entraIdOptions)
+ {
+ _httpClientFactory = httpClientFactory;
+ _httpContextAccessor = httpContextAccessor;
+ _configuration = configuration;
+ _entraIdConfig = entraIdOptions.Value;
+ }
+
+ ///
+ /// Retrieves SharePoint site information including title, description, URL, creation date, and language.
+ ///
+ /// The SharePoint site URL.
+ /// Formatted site information as a string.
+ [McpServerTool, Description("Get SharePoint site information using SharePoint REST API.")]
+ public async Task GetSiteInfo(
+ [Description("The SharePoint site URL (e.g., https://contoso.sharepoint.com/sites/sitename)")] string siteUrl)
+ {
+ var spoAccessToken = await GetSharePointTokenAsync(siteUrl);
+ var client = _httpClientFactory.CreateClient();
+
+ client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", spoAccessToken);
+ client.DefaultRequestHeaders.Add("Accept", "application/json;odata=verbose");
+
+ var apiUrl = $"{siteUrl.TrimEnd('/')}/_api/web";
+ var response = await client.GetAsync(apiUrl);
+
+ if (!response.IsSuccessStatusCode)
+ {
+ throw new McpException($"SharePoint REST API call failed: {response.StatusCode} - {await response.Content.ReadAsStringAsync()}");
+ }
+
+ var jsonContent = await response.Content.ReadAsStringAsync();
+ using var document = JsonDocument.Parse(jsonContent);
+ var webInfo = document.RootElement.GetProperty("d");
+
+ var title = webInfo.TryGetProperty("Title", out var titleElement) ? titleElement.GetString() : "Unknown";
+ var description = webInfo.TryGetProperty("Description", out var descElement) ? descElement.GetString() : "";
+ var url = webInfo.TryGetProperty("Url", out var urlElement) ? urlElement.GetString() : "";
+ var created = webInfo.TryGetProperty("Created", out var createdElement) ? createdElement.GetString() : "";
+ var language = webInfo.TryGetProperty("Language", out var langElement) ? langElement.GetInt32() : 0;
+
+ return $"""
+ Site Title: {title}
+ Description: {description}
+ URL: {url}
+ Created: {created}
+ Language: {language}
+ """;
+ }
+
+ ///
+ /// Exchanges an MCP access token for a SharePoint access token using On-Behalf-Of flow.
+ ///
+ /// The SharePoint site URL to determine the resource scope.
+ /// A SharePoint access token.
+ private async Task GetSharePointTokenAsync(string siteUrl)
+ {
+ var httpContext = _httpContextAccessor.HttpContext
+ ?? throw new McpException("HTTP context not available");
+
+ var authHeader = httpContext.Request.Headers.Authorization.FirstOrDefault();
+ if (string.IsNullOrEmpty(authHeader) || !authHeader.StartsWith("Bearer "))
+ {
+ throw new McpException("No valid bearer token found in request");
+ }
+
+ var mcpAccessToken = authHeader["Bearer ".Length..];
+
+ // Validate required configuration
+ if (string.IsNullOrEmpty(_entraIdConfig.TenantId))
+ {
+ throw new McpException("EntraId:TenantId configuration is required for On-Behalf-Of flow");
+ }
+
+ if (string.IsNullOrEmpty(_entraIdConfig.ClientId))
+ {
+ throw new McpException("EntraId:ClientId configuration is required for On-Behalf-Of flow");
+ }
+
+ // Get client secret from configuration (should be in user secrets or secure storage)
+ var clientSecret = _entraIdConfig.ClientSecret ?? _configuration["AzureAd:ClientSecret"];
+ if (string.IsNullOrEmpty(clientSecret))
+ {
+ throw new McpException("Client secret not configured for On-Behalf-Of flow. Check EntraId:ClientSecret or AzureAd:ClientSecret configuration.");
+ }
+
+ // Extract SharePoint resource from URL
+ var uri = new Uri(siteUrl);
+ var spoResource = $"https://{uri.Host}";
+
+ var client = _httpClientFactory.CreateClient();
+
+ var tokenEndpoint = _entraIdConfig.TokenEndpoint;
+
+ var requestParams = new Dictionary
+ {
+ ["grant_type"] = "urn:ietf:params:oauth:grant-type:jwt-bearer",
+ ["client_id"] = _entraIdConfig.ClientId,
+ ["client_secret"] = clientSecret,
+ ["assertion"] = mcpAccessToken,
+ ["scope"] = $"{spoResource}/.default",
+ ["requested_token_use"] = "on_behalf_of"
+ };
+
+ var requestContent = new FormUrlEncodedContent(requestParams);
+ var response = await client.PostAsync(tokenEndpoint, requestContent);
+
+ if (!response.IsSuccessStatusCode)
+ {
+ var errorContent = await response.Content.ReadAsStringAsync();
+ throw new McpException($"SharePoint On-Behalf-Of token exchange failed: {response.StatusCode} - {errorContent}");
+ }
+
+ var responseContent = await response.Content.ReadAsStringAsync();
+ using var document = JsonDocument.Parse(responseContent);
+
+ if (!document.RootElement.TryGetProperty("access_token", out var tokenElement))
+ {
+ throw new McpException("No access token in SharePoint On-Behalf-Of response");
+ }
+
+ return tokenElement.GetString() ?? throw new McpException("SharePoint access token is null");
+ }
+}
\ No newline at end of file
diff --git a/samples/EntraProtectedMcpServer/Tools/WeatherTools.cs b/samples/EntraProtectedMcpServer/Tools/WeatherTools.cs
new file mode 100644
index 000000000..9b7fe28dd
--- /dev/null
+++ b/samples/EntraProtectedMcpServer/Tools/WeatherTools.cs
@@ -0,0 +1,70 @@
+using ModelContextProtocol;
+using ModelContextProtocol.Server;
+using System.ComponentModel;
+using System.Globalization;
+using System.Text.Json;
+
+namespace EntraProtectedMcpServer.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 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 jsonDocument = await client.GetFromJsonAsync($"/alerts/active/area/{state}")
+ ?? 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 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 locationDocument = await client.GetFromJsonAsync(pointUrl);
+ var forecastUrl = locationDocument?.RootElement.GetProperty("properties").GetProperty("forecast").GetString()
+ ?? throw new McpException($"No forecast URL provided by {client.BaseAddress}points/{latitude},{longitude}");
+
+ using var forecastDocument = await client.GetFromJsonAsync(forecastUrl);
+ 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()}
+ """));
+ }
+}
diff --git a/samples/EntraProtectedMcpServer/appsettings.Development.json b/samples/EntraProtectedMcpServer/appsettings.Development.json
new file mode 100644
index 000000000..f999bc20e
--- /dev/null
+++ b/samples/EntraProtectedMcpServer/appsettings.Development.json
@@ -0,0 +1,9 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Debug",
+ "System": "Information",
+ "Microsoft": "Information"
+ }
+ }
+}
\ No newline at end of file
diff --git a/samples/EntraProtectedMcpServer/appsettings.json b/samples/EntraProtectedMcpServer/appsettings.json
new file mode 100644
index 000000000..9c0e5642f
--- /dev/null
+++ b/samples/EntraProtectedMcpServer/appsettings.json
@@ -0,0 +1,23 @@
+{
+ "Server": {
+ "Url": "http://localhost:7071/",
+ "ResourceDocumentationUrl": "https://docs.example.com/api/weather"
+ },
+ "EntraId": {
+ "TenantId": "your-tenant-id-here",
+ "ClientId": "your-client-id-here",
+ "AuthorityUrl": "https://login.microsoftonline.com/your-tenant-id-here/v2.0",
+ "TokenEndpoint": "https://login.microsoftonline.com/your-tenant-id-here/oauth2/v2.0/token",
+ "ScopesSupported": [
+ "user_impersonation" // The scope your MCP server exposes
+ ]
+ },
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning",
+ "Microsoft.Hosting.Lifetime": "Information"
+ }
+ },
+ "AllowedHosts": "*"
+}
\ No newline at end of file
diff --git a/src/ModelContextProtocol.Core/Authentication/ClientOAuthProvider.cs b/src/ModelContextProtocol.Core/Authentication/ClientOAuthProvider.cs
index 468728982..c1646acb8 100644
--- a/src/ModelContextProtocol.Core/Authentication/ClientOAuthProvider.cs
+++ b/src/ModelContextProtocol.Core/Authentication/ClientOAuthProvider.cs
@@ -300,14 +300,26 @@ private async Task GetAuthServerMetadataAsync(Uri a
private async Task RefreshTokenAsync(string refreshToken, Uri resourceUri, AuthorizationServerMetadata authServerMetadata, CancellationToken cancellationToken)
{
- var requestContent = new FormUrlEncodedContent(new Dictionary
+ var requestParams = new Dictionary
{
["grant_type"] = "refresh_token",
["refresh_token"] = refreshToken,
["client_id"] = GetClientIdOrThrow(),
["client_secret"] = _clientSecret ?? string.Empty,
- ["resource"] = resourceUri.ToString(),
- });
+ };
+
+ // Only add resource parameter for non-Entra servers
+ if (authServerMetadata.Issuer?.Host != "login.microsoftonline.com")
+ {
+ requestParams["resource"] = resourceUri.ToString();
+ }
+ // For Microsoft Entra ID, use scope instead of resource
+ else if (_scopes is not null && _scopes.Length > 0)
+ {
+ requestParams["scope"] = string.Join(" ", _scopes);
+ }
+
+ var requestContent = new FormUrlEncodedContent(requestParams);
using var request = new HttpRequestMessage(HttpMethod.Post, authServerMetadata.TokenEndpoint)
{
@@ -384,7 +396,7 @@ private async Task ExchangeCodeForTokenAsync(
string codeVerifier,
CancellationToken cancellationToken)
{
- var requestContent = new FormUrlEncodedContent(new Dictionary
+ var requestParams = new Dictionary
{
["grant_type"] = "authorization_code",
["code"] = authorizationCode,
@@ -392,8 +404,20 @@ private async Task ExchangeCodeForTokenAsync(
["client_id"] = GetClientIdOrThrow(),
["code_verifier"] = codeVerifier,
["client_secret"] = _clientSecret ?? string.Empty,
- ["resource"] = protectedResourceMetadata.Resource.ToString(),
- });
+ };
+
+ // Only add resource parameter for non-Entra servers
+ if (authServerMetadata.Issuer?.Host != "login.microsoftonline.com")
+ {
+ requestParams["resource"] = protectedResourceMetadata.Resource.ToString();
+ }
+ // For Microsoft Entra ID, use scope instead of resource
+ else if (_scopes is not null && _scopes.Length > 0)
+ {
+ requestParams["scope"] = string.Join(" ", _scopes);
+ }
+
+ var requestContent = new FormUrlEncodedContent(requestParams);
using var request = new HttpRequestMessage(HttpMethod.Post, authServerMetadata.TokenEndpoint)
{