Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions MCPify/Core/McpifyOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,11 @@ public class McpifyOptions
/// Defaults to <see cref="BrowserLaunchBehavior.Auto"/> which detects headless environments at runtime.
/// </summary>
public BrowserLaunchBehavior LoginBrowserBehavior { get; set; } = BrowserLaunchBehavior.Auto;

/// <summary>
/// Optional list of OAuth2 configurations to be added to the OAuthConfigurationStore.
/// </summary>
public List<OAuth2Configuration> OAuthConfigurations { get; set; } = new();
}

/// <summary>
Expand Down
2 changes: 1 addition & 1 deletion MCPify/Hosting/McpOAuthAuthenticationMiddleware.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
32 changes: 17 additions & 15 deletions MCPify/Hosting/McpifyEndpointExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -129,27 +129,29 @@ string BaseUrlProvider()

static IEnumerable<string> 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<string> 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
{
Expand Down
8 changes: 7 additions & 1 deletion MCPify/Hosting/McpifyServiceExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,13 @@ public static IServiceCollection AddMcpify(
});

services.AddSingleton<OpenApiOAuthParser>();
services.AddSingleton<OAuthConfigurationStore>();

var oauthStore = new OAuthConfigurationStore();
foreach (var config in opts.OAuthConfigurations)
{
oauthStore.AddConfiguration(config);
}
services.AddSingleton(oauthStore);

return services;
}
Expand Down
2 changes: 1 addition & 1 deletion Sample/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
4 changes: 2 additions & 2 deletions Tests/MCPify.Tests/Integration/OAuthMiddlewareTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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]
Expand Down