diff --git a/MCPify/Core/McpifyOptions.cs b/MCPify/Core/McpifyOptions.cs
index c769539..0b728c0 100644
--- a/MCPify/Core/McpifyOptions.cs
+++ b/MCPify/Core/McpifyOptions.cs
@@ -64,6 +64,11 @@ public class McpifyOptions
/// Defaults to which detects headless environments at runtime.
///
public BrowserLaunchBehavior LoginBrowserBehavior { get; set; } = BrowserLaunchBehavior.Auto;
+
+ ///
+ /// Optional list of OAuth2 configurations to be added to the OAuthConfigurationStore.
+ ///
+ public List OAuthConfigurations { get; set; } = new();
}
///
diff --git a/MCPify/Hosting/McpOAuthAuthenticationMiddleware.cs b/MCPify/Hosting/McpOAuthAuthenticationMiddleware.cs
index ffd3885..ed5c35c 100644
--- a/MCPify/Hosting/McpOAuthAuthenticationMiddleware.cs
+++ b/MCPify/Hosting/McpOAuthAuthenticationMiddleware.cs
@@ -63,7 +63,7 @@ public async Task InvokeAsync(HttpContext context)
context.Response.StatusCode = StatusCodes.Status401Unauthorized;
context.Response.Headers[HeaderNames.WWWAuthenticate] =
- $"Bearer realm=\"MCPify\", resource=\"{resourceUrl}\", resource_metadata_url=\"{metadataUrl}\"";
+ $"Bearer realm=\"MCPify\", resource=\"{resourceUrl}\", resource_metadata=\"{metadataUrl}\"";
return;
}
diff --git a/MCPify/Hosting/McpifyEndpointExtensions.cs b/MCPify/Hosting/McpifyEndpointExtensions.cs
index c01e37f..9837f3b 100644
--- a/MCPify/Hosting/McpifyEndpointExtensions.cs
+++ b/MCPify/Hosting/McpifyEndpointExtensions.cs
@@ -129,27 +129,29 @@ string BaseUrlProvider()
static IEnumerable ResolveAuthorizationServers(OAuth2Configuration config)
{
- if (config.AuthorizationServers.Count > 0)
- {
- foreach (var server in config.AuthorizationServers)
- {
- yield return server;
- }
-
- yield break;
- }
-
if (Uri.TryCreate(config.AuthorizationUrl, UriKind.Absolute, out var uri))
{
yield return uri.GetLeftPart(UriPartial.Authority);
}
}
- // Prefer explicitly configured authorization servers, fall back to derived authorities.
- var issuers = configs
- .SelectMany(ResolveAuthorizationServers)
- .Distinct(StringComparer.OrdinalIgnoreCase)
- .ToList();
+ // If any config has AuthorizationServers, only use those; otherwise, fall back to AuthorizationUrl authority.
+ List issuers;
+ if (configs.Any(c => c.AuthorizationServers?.Any() == true))
+ {
+ issuers = configs
+ .Where(c => c.AuthorizationServers?.Any() == true)
+ .SelectMany(c => c.AuthorizationServers)
+ .Distinct(StringComparer.OrdinalIgnoreCase)
+ .ToList();
+ }
+ else
+ {
+ issuers = configs
+ .SelectMany(ResolveAuthorizationServers)
+ .Distinct(StringComparer.OrdinalIgnoreCase)
+ .ToList();
+ }
return Results.Ok(new
{
diff --git a/MCPify/Hosting/McpifyServiceExtensions.cs b/MCPify/Hosting/McpifyServiceExtensions.cs
index 23a57c1..b79f0a2 100644
--- a/MCPify/Hosting/McpifyServiceExtensions.cs
+++ b/MCPify/Hosting/McpifyServiceExtensions.cs
@@ -95,7 +95,13 @@ public static IServiceCollection AddMcpify(
});
services.AddSingleton();
- services.AddSingleton();
+
+ var oauthStore = new OAuthConfigurationStore();
+ foreach (var config in opts.OAuthConfigurations)
+ {
+ oauthStore.AddConfiguration(config);
+ }
+ services.AddSingleton(oauthStore);
return services;
}
diff --git a/Sample/README.md b/Sample/README.md
index 3a874f6..bf13419 100644
--- a/Sample/README.md
+++ b/Sample/README.md
@@ -80,7 +80,7 @@ To run the server in HTTP mode (using Server-Sent Events):
This sample demonstrates how clients can authenticate with MCPify using OAuth 2.0 Authorization Code flow.
-1. **Discover Authentication**: When an unauthenticated client attempts to use a protected tool (e.g., `api_secrets_get`), MCPify will respond with a `401 Unauthorized` HTTP status code and a `WWW-Authenticate` header, including `resource_metadata_url`. The client should then fetch this metadata.
+1. **Discover Authentication**: When an unauthenticated client attempts to use a protected tool (e.g., `api_secrets_get`), MCPify will respond with a `401 Unauthorized` HTTP status code and a `WWW-Authenticate` header, including `resource_metadata`. The client should then fetch this metadata.
2. **Initiate Login**: The client (e.g., Claude Desktop) will call the `login_auth_code_pkce` tool provided by MCPify. This tool returns an authorization URL.
3. **User Authorization**: The user opens the authorization URL in a browser, logs in (using the OpenIddict provider in this sample), and grants consent.
4. **Callback and Token Exchange**: After user authorization, the browser redirects to MCPify's callback endpoint (`/auth/callback`). MCPify handles the code exchange and stores the token securely for the specific session.
diff --git a/Tests/MCPify.Tests/Integration/OAuthMiddlewareTests.cs b/Tests/MCPify.Tests/Integration/OAuthMiddlewareTests.cs
index 37653ac..04cfe31 100644
--- a/Tests/MCPify.Tests/Integration/OAuthMiddlewareTests.cs
+++ b/Tests/MCPify.Tests/Integration/OAuthMiddlewareTests.cs
@@ -29,7 +29,7 @@ public async Task Request_Returns401_WhenNoToken_And_OAuthConfigured()
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
Assert.Contains("WWW-Authenticate", response.Headers.ToDictionary(h => h.Key, h => string.Join(", ", h.Value)).Keys);
var authHeader = response.Headers.WwwAuthenticate.ToString();
- Assert.Contains("resource_metadata_url", authHeader);
+ Assert.Contains("resource_metadata", authHeader);
}
[Fact]
@@ -53,7 +53,7 @@ public async Task Request_Challenge_UsesResourceOverride()
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
var authHeader = response.Headers.WwwAuthenticate.ToString();
Assert.Contains($"resource=\"{publicUrl}\"", authHeader);
- Assert.Contains($"resource_metadata_url=\"{publicUrl}/.well-known/oauth-protected-resource\"", authHeader);
+ Assert.Contains($"resource_metadata=\"{publicUrl}/.well-known/oauth-protected-resource\"", authHeader);
}
[Fact]