Skip to content
Closed
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
15 changes: 15 additions & 0 deletions MCPify/Core/Auth/ITokenProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
namespace MCPify.Core.Auth;

/// <summary>
/// Abstraction for retrieving authentication tokens.
/// Separates token acquisition from token application.
/// </summary>
public interface ITokenProvider
{
/// <summary>
/// Retrieves an authentication token if available.
/// </summary>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>The token string, or null if no token is available</returns>
Task<string?> GetTokenAsync(CancellationToken cancellationToken = default);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
namespace MCPify.Core.Auth.TokenProviders;

/// <summary>
/// Token provider that wraps existing IAuthenticationProvider implementations.
/// Used for server-managed authentication (OAuth, API keys, etc.).
/// </summary>
public class AuthenticationFactoryTokenProvider : ITokenProvider
{
private readonly Func<IServiceProvider, IAuthenticationProvider>? _authenticationFactory;
private readonly IServiceProvider _serviceProvider;

public AuthenticationFactoryTokenProvider(
Func<IServiceProvider, IAuthenticationProvider>? authenticationFactory,
IServiceProvider serviceProvider)
{
_authenticationFactory = authenticationFactory;
_serviceProvider = serviceProvider;
}

public async Task<string?> GetTokenAsync(CancellationToken cancellationToken = default)
{
if (_authenticationFactory == null)
{
return null;
}

var authProvider = _authenticationFactory.Invoke(_serviceProvider);

using var tempRequest = new HttpRequestMessage();
await authProvider.ApplyAsync(tempRequest, cancellationToken);

var authHeader = tempRequest.Headers.Authorization;
if (authHeader != null)
{
return $"{authHeader.Scheme} {authHeader.Parameter}";
}

return null;
}
}
29 changes: 29 additions & 0 deletions MCPify/Core/Auth/TokenProviders/CompositeTokenProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
namespace MCPify.Core.Auth.TokenProviders;

/// <summary>
/// Token provider that tries multiple providers in order until one returns a token.
/// Useful for implementing fallback behavior (e.g., try client token first, then server).
/// </summary>
public class CompositeTokenProvider : ITokenProvider
{
private readonly IEnumerable<ITokenProvider> _providers;

public CompositeTokenProvider(IEnumerable<ITokenProvider> providers)
{
_providers = providers ?? throw new ArgumentNullException(nameof(providers));
}

public async Task<string?> GetTokenAsync(CancellationToken cancellationToken = default)
{
foreach (var provider in _providers)
{
var token = await provider.GetTokenAsync(cancellationToken);
if (!string.IsNullOrEmpty(token))
{
return token;
}
}

return null;
}
}
27 changes: 27 additions & 0 deletions MCPify/Core/Auth/TokenProviders/McpContextTokenProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
namespace MCPify.Core.Auth.TokenProviders;

/// <summary>
/// Token provider that retrieves tokens from the MCP context.
/// Used when the MCP client manages authentication and provides tokens.
/// </summary>
public class McpContextTokenProvider : ITokenProvider
{
private readonly IMcpContextAccessor _mcpContextAccessor;

public McpContextTokenProvider(IMcpContextAccessor mcpContextAccessor)
{
_mcpContextAccessor = mcpContextAccessor;
}

public Task<string?> GetTokenAsync(CancellationToken cancellationToken = default)
{
var token = _mcpContextAccessor.AccessToken;

if (!string.IsNullOrEmpty(token) && !token.Contains(' '))
{
token = $"Bearer {token}";
}

return Task.FromResult(token);
}
}
22 changes: 22 additions & 0 deletions MCPify/Core/Auth/TokenProviders/NoTokenProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
namespace MCPify.Core.Auth.TokenProviders;

/// <summary>
/// Token provider that provides no authentication token.
/// Used when no authentication is required (e.g., public APIs).
/// </summary>
public sealed class NoTokenProvider : ITokenProvider
{
/// <summary>
/// Singleton instance of the no-token provider.
/// </summary>
public static readonly NoTokenProvider Instance = new();

private NoTokenProvider()
{
}

public Task<string?> GetTokenAsync(CancellationToken cancellationToken = default)
{
return Task.FromResult<string?>(null);
}
}
38 changes: 38 additions & 0 deletions MCPify/Core/Auth/TokenProviders/TokenProviderFactory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
namespace MCPify.Core.Auth.TokenProviders;

/// <summary>
/// Factory for creating ITokenProvider instances based on TokenSource configuration.
/// </summary>
public static class TokenProviderFactory
{
/// <summary>
/// Creates an ITokenProvider based on the specified TokenSource and authentication factory.
/// </summary>
/// <param name="serviceProvider">Service provider for resolving dependencies</param>
/// <param name="tokenSource">The token source configuration</param>
/// <param name="authenticationFactory">Optional authentication factory for server-managed auth</param>
/// <returns>An ITokenProvider instance</returns>
public static ITokenProvider Create(
IServiceProvider serviceProvider,
TokenSource tokenSource,
Func<IServiceProvider, IAuthenticationProvider>? authenticationFactory = null)
{
return tokenSource switch
{
TokenSource.Server => new AuthenticationFactoryTokenProvider(authenticationFactory, serviceProvider),

TokenSource.Client => new McpContextTokenProvider(
serviceProvider.GetRequiredService<IMcpContextAccessor>()),

TokenSource.Both => new CompositeTokenProvider(new ITokenProvider[]
{
new McpContextTokenProvider(serviceProvider.GetRequiredService<IMcpContextAccessor>()),
new AuthenticationFactoryTokenProvider(authenticationFactory, serviceProvider)
}),

TokenSource.None => NoTokenProvider.Instance,

_ => throw new ArgumentOutOfRangeException(nameof(tokenSource), tokenSource, "Invalid TokenSource value")
};
}
}
41 changes: 41 additions & 0 deletions MCPify/Core/McpifyOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,35 @@ public class McpifyOptions

}

/// <summary>
/// Defines where authentication tokens should be sourced from.
/// </summary>
public enum TokenSource
{
/// <summary>
/// MCPify server manages authentication (OAuth flows, API keys, etc.).
/// This is the default for backward compatibility.
/// </summary>
Server,

/// <summary>
/// MCP client provides the authentication token directly.
/// Use this when the client handles OAuth or other authentication flows.
/// </summary>
Client,

/// <summary>
/// Try client token first, then fall back to server authentication.
/// Useful for hybrid scenarios.
/// </summary>
Both,

/// <summary>
/// No authentication required.
/// </summary>
None
}

/// <summary>
/// Defines the available transport types for the MCP server.
/// </summary>
Expand Down Expand Up @@ -142,6 +171,12 @@ public class LocalEndpointsOptions
/// A factory for creating an authentication provider for local endpoints.
/// </summary>
public Func<IServiceProvider, IAuthenticationProvider>? AuthenticationFactory { get; set; }

/// <summary>
/// Specifies where authentication tokens should be sourced from.
/// Defaults to Both (hybrid) - tries client token first, then server authentication.
/// </summary>
public TokenSource TokenSource { get; set; } = TokenSource.Both;
}

/// <summary>
Expand Down Expand Up @@ -183,4 +218,10 @@ public class ExternalApiOptions
/// A factory for creating an authentication provider for this API.
/// </summary>
public Func<IServiceProvider, IAuthenticationProvider>? AuthenticationFactory { get; set; }

/// <summary>
/// Specifies where authentication tokens should be sourced from.
/// Defaults to Both (hybrid) - tries client token first, then server authentication.
/// </summary>
public TokenSource TokenSource { get; set; } = TokenSource.Both;
}
4 changes: 3 additions & 1 deletion MCPify/Hosting/McpifyEndpointExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using MCPify.Core;
using MCPify.Core.Auth;
using MCPify.Core.Auth.TokenProviders;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting.Server;
using Microsoft.AspNetCore.Hosting.Server.Features;
Expand Down Expand Up @@ -87,7 +88,8 @@ string BaseUrlProvider()
DefaultHeaders = options.LocalEndpoints.DefaultHeaders
};

var tool = new OpenApiProxyTool(descriptor, BaseUrlProvider, httpClient, services.GetRequiredService<IJsonSchemaGenerator>(), localOpts, options.LocalEndpoints.AuthenticationFactory);
var tokenProvider = TokenProviderFactory.Create(services, options.LocalEndpoints.TokenSource, options.LocalEndpoints.AuthenticationFactory);
var tool = new OpenApiProxyTool(descriptor, BaseUrlProvider, httpClient, services.GetRequiredService<IJsonSchemaGenerator>(), localOpts, tokenProvider);
toolCollection.Add(tool);
count++;
}
Expand Down
7 changes: 5 additions & 2 deletions MCPify/Hosting/McpifyServiceRegistrar.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using MCPify.Schema;
using MCPify.Tools;
using MCPify.Core.Auth;
using MCPify.Core.Auth.TokenProviders;
using Microsoft.AspNetCore.Hosting.Server;
using Microsoft.AspNetCore.Hosting.Server.Features;
using Microsoft.AspNetCore.Routing;
Expand Down Expand Up @@ -146,7 +147,8 @@ private async Task RegisterExternalEndpointsAsync()
apiOpts.DefaultHeaders[header.Key] = header.Value;
}

var tool = new OpenApiProxyTool(descriptor, apiOptions.ApiBaseUrl, httpClient, _schema, apiOpts, apiOptions.AuthenticationFactory);
var tokenProvider = TokenProviderFactory.Create(_serviceProvider, apiOptions.TokenSource, apiOptions.AuthenticationFactory);
var tool = new OpenApiProxyTool(descriptor, apiOptions.ApiBaseUrl, httpClient, _schema, apiOpts, tokenProvider);
var decoratedTool = new SessionAwareToolDecorator(tool, _serviceProvider);
toolCollection.Add(decoratedTool);
count++;
Expand Down Expand Up @@ -215,7 +217,8 @@ string BaseUrlProvider()
? _options.LocalEndpoints.AuthenticationFactory
: null;

var tool = new OpenApiProxyTool(descriptor, BaseUrlProvider, httpClient, _schema, localOpts, effectiveAuthFactory);
var tokenProvider = TokenProviderFactory.Create(_serviceProvider, _options.LocalEndpoints.TokenSource, effectiveAuthFactory);
var tool = new OpenApiProxyTool(descriptor, BaseUrlProvider, httpClient, _schema, localOpts, tokenProvider);
var decoratedTool = new SessionAwareToolDecorator(tool, _serviceProvider);
toolCollection.Add(decoratedTool);
count++;
Expand Down
2 changes: 1 addition & 1 deletion MCPify/MCPify.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<IsPackable>true</IsPackable>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<PackageId>MCPify</PackageId>
<Version>1.0.0-preview.1</Version>
<Version>0.0.13-preview</Version>
<Authors>Abdullah D.</Authors>
<Description>Turn any ASP.NET Core API or OpenAPI spec into a Model Context Protocol (MCP) server. Seamlessly integrates with Claude Desktop and other MCP clients.</Description>
<PackageTags>mcp;openapi;swagger;api;tools</PackageTags>
Expand Down
25 changes: 17 additions & 8 deletions MCPify/Tools/OpenApiProxyTool.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using Microsoft.OpenApi.Models;
using ModelContextProtocol.Protocol;
using ModelContextProtocol.Server;
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
Expand All @@ -20,7 +21,7 @@ public class OpenApiProxyTool : McpServerTool
private readonly Func<string> _apiBaseUrlProvider;
private readonly OpenApiOperationDescriptor _descriptor;
private readonly McpifyOptions _options;
private readonly Func<IServiceProvider, IAuthenticationProvider>? _authenticationFactory;
private readonly ITokenProvider _tokenProvider;
private IReadOnlyList<object>? _metadata;

public OpenApiProxyTool(
Expand All @@ -29,8 +30,8 @@ public OpenApiProxyTool(
HttpClient http,
IJsonSchemaGenerator schema,
McpifyOptions options,
Func<IServiceProvider, IAuthenticationProvider>? authenticationFactory = null)
: this(descriptor, () => apiBaseUrl, http, schema, options, authenticationFactory)
ITokenProvider tokenProvider)
: this(descriptor, () => apiBaseUrl, http, schema, options, tokenProvider)
{
}

Expand All @@ -40,14 +41,14 @@ public OpenApiProxyTool(
HttpClient http,
IJsonSchemaGenerator schema,
McpifyOptions options,
Func<IServiceProvider, IAuthenticationProvider>? authenticationFactory = null)
ITokenProvider tokenProvider)
{
_descriptor = descriptor;
_apiBaseUrlProvider = apiBaseUrlProvider;
_http = http;
_schema = schema;
_options = options;
_authenticationFactory = authenticationFactory;
_tokenProvider = tokenProvider ?? throw new ArgumentNullException(nameof(tokenProvider));
}

public override Tool ProtocolTool => new()
Expand All @@ -74,10 +75,18 @@ public override async ValueTask<CallToolResult> InvokeAsync(
var request = BuildHttpRequest(argsDict);
logger?.LogInformation("Invoking {Method} {Url}", request.Method, request.RequestUri);

if (_authenticationFactory != null)
var authToken = await _tokenProvider.GetTokenAsync(token);
if (!string.IsNullOrEmpty(authToken))
{
var authentication = _authenticationFactory.Invoke(context.Services!);
await authentication.ApplyAsync(request, token);
var parts = authToken.Split(' ', 2);
if (parts.Length == 2)
{
request.Headers.Authorization = new AuthenticationHeaderValue(parts[0], parts[1]);
}
else
{
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", authToken);
}
}

var response = await _http.SendAsync(request, token);
Expand Down
Loading