Skip to content

Commit c7183f6

Browse files
committed
Exploratory implementation
1 parent 4fd3ebf commit c7183f6

18 files changed

+1634
-39
lines changed

.gitignore

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,4 +80,7 @@ docs/api
8080

8181
# Rider
8282
.idea/
83-
.idea_modules/
83+
.idea_modules/
84+
85+
# Specs
86+
.specs/
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<OutputType>Exe</OutputType>
5+
<TargetFramework>net8.0</TargetFramework>
6+
<ImplicitUsings>enable</ImplicitUsings>
7+
<Nullable>enable</Nullable>
8+
</PropertyGroup>
9+
10+
<ItemGroup>
11+
<ProjectReference Include="..\..\src\ModelContextProtocol\ModelContextProtocol.csproj" />
12+
</ItemGroup>
13+
14+
</Project>
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
using System.Diagnostics;
2+
using ModelContextProtocol.Client;
3+
using ModelContextProtocol.Protocol.Transport;
4+
5+
namespace AuthorizationExample;
6+
7+
/// <summary>
8+
/// Example demonstrating how to use the MCP C# SDK with OAuth authorization.
9+
/// </summary>
10+
public class Program
11+
{
12+
public static async Task Main(string[] args)
13+
{
14+
// Define the MCP server endpoint that requires OAuth authentication
15+
var serverEndpoint = new Uri("https://example.com/mcp");
16+
17+
// Set up the SSE transport with authorization support
18+
var transportOptions = new SseClientTransportOptions
19+
{
20+
Endpoint = serverEndpoint,
21+
22+
// Provide a callback to handle the authorization flow
23+
AuthorizeCallback = async (clientMetadata) =>
24+
{
25+
Console.WriteLine("Authentication required. Opening browser for authorization...");
26+
27+
// In a real app, you'd likely have a local HTTP server to receive the callback
28+
// This is just a simplified example
29+
Console.WriteLine("Once you've authorized in the browser, enter the code and redirect URI:");
30+
Console.Write("Code: ");
31+
var code = Console.ReadLine() ?? "";
32+
Console.Write("Redirect URI: ");
33+
var redirectUri = Console.ReadLine() ?? "http://localhost:8888/callback";
34+
35+
return (redirectUri, code);
36+
}
37+
38+
// Alternatively, use the built-in local server handler:
39+
// AuthorizeCallback = SseClientTransport.CreateLocalServerAuthorizeCallback(
40+
// openBrowser: async (url) =>
41+
// {
42+
// // Open the URL in the user's default browser
43+
// Process.Start(new ProcessStartInfo(url) { UseShellExecute = true });
44+
// }
45+
// )
46+
};
47+
48+
try
49+
{
50+
// Create the client with authorization-enabled transport
51+
var transport = new SseClientTransport(transportOptions);
52+
var client = await McpClient.CreateAsync(transport);
53+
54+
// Use the MCP client normally - authorization is handled automatically
55+
// If the server returns a 401 Unauthorized response, the authorization flow will be triggered
56+
var result = await client.PingAsync();
57+
Console.WriteLine($"Server ping successful: {result.ServerInfo.Name} {result.ServerInfo.Version}");
58+
59+
// Example tool call
60+
var weatherPrompt = "What's the weather like today?";
61+
var weatherResult = await client.CompletionCompleteAsync(
62+
new CompletionCompleteRequestBuilder(weatherPrompt).Build());
63+
64+
Console.WriteLine($"Response: {weatherResult.Content.Text}");
65+
}
66+
catch (McpAuthorizationException authEx)
67+
{
68+
Console.WriteLine($"Authorization error: {authEx.Message}");
69+
Console.WriteLine($"Resource: {authEx.ResourceUri}");
70+
Console.WriteLine($"Auth server: {authEx.AuthorizationServerUri}");
71+
}
72+
catch (McpException mcpEx)
73+
{
74+
Console.WriteLine($"MCP error: {mcpEx.Message} (Error code: {mcpEx.ErrorCode})");
75+
}
76+
catch (Exception ex)
77+
{
78+
Console.WriteLine($"Unexpected error: {ex.Message}");
79+
}
80+
}
81+
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
namespace ModelContextProtocol;
2+
3+
/// <summary>
4+
/// Represents an exception that is thrown when an authorization or authentication error occurs in MCP.
5+
/// </summary>
6+
/// <remarks>
7+
/// This exception is thrown when the client fails to authenticate with an MCP server that requires
8+
/// authentication, such as when the OAuth authorization flow fails or when the server rejects the provided credentials.
9+
/// </remarks>
10+
public class McpAuthorizationException : McpException
11+
{
12+
/// <summary>
13+
/// Initializes a new instance of the <see cref="McpAuthorizationException"/> class.
14+
/// </summary>
15+
public McpAuthorizationException()
16+
: base("Authorization failed", McpErrorCode.Unauthorized)
17+
{
18+
}
19+
20+
/// <summary>
21+
/// Initializes a new instance of the <see cref="McpAuthorizationException"/> class with a specified error message.
22+
/// </summary>
23+
/// <param name="message">The message that describes the error.</param>
24+
public McpAuthorizationException(string message)
25+
: base(message, McpErrorCode.Unauthorized)
26+
{
27+
}
28+
29+
/// <summary>
30+
/// Initializes a new instance of the <see cref="McpAuthorizationException"/> class with a specified error message and a reference to the inner exception that is the cause of this exception.
31+
/// </summary>
32+
/// <param name="message">The message that describes the error.</param>
33+
/// <param name="innerException">The exception that is the cause of the current exception, or a null reference if no inner exception is specified.</param>
34+
public McpAuthorizationException(string message, Exception? innerException)
35+
: base(message, innerException, McpErrorCode.Unauthorized)
36+
{
37+
}
38+
39+
/// <summary>
40+
/// Initializes a new instance of the <see cref="McpAuthorizationException"/> class with a specified error message and error code.
41+
/// </summary>
42+
/// <param name="message">The message that describes the error.</param>
43+
/// <param name="errorCode">The MCP error code. Should be either <see cref="McpErrorCode.Unauthorized"/> or <see cref="McpErrorCode.AuthenticationFailed"/>.</param>
44+
public McpAuthorizationException(string message, McpErrorCode errorCode)
45+
: base(message, errorCode)
46+
{
47+
if (errorCode != McpErrorCode.Unauthorized && errorCode != McpErrorCode.AuthenticationFailed)
48+
{
49+
throw new ArgumentException($"Error code must be either {nameof(McpErrorCode.Unauthorized)} or {nameof(McpErrorCode.AuthenticationFailed)}", nameof(errorCode));
50+
}
51+
}
52+
53+
/// <summary>
54+
/// Initializes a new instance of the <see cref="McpAuthorizationException"/> class with a specified error message, inner exception, and error code.
55+
/// </summary>
56+
/// <param name="message">The message that describes the error.</param>
57+
/// <param name="innerException">The exception that is the cause of the current exception, or a null reference if no inner exception is specified.</param>
58+
/// <param name="errorCode">The MCP error code. Should be either <see cref="McpErrorCode.Unauthorized"/> or <see cref="McpErrorCode.AuthenticationFailed"/>.</param>
59+
public McpAuthorizationException(string message, Exception? innerException, McpErrorCode errorCode)
60+
: base(message, innerException, errorCode)
61+
{
62+
if (errorCode != McpErrorCode.Unauthorized && errorCode != McpErrorCode.AuthenticationFailed)
63+
{
64+
throw new ArgumentException($"Error code must be either {nameof(McpErrorCode.Unauthorized)} or {nameof(McpErrorCode.AuthenticationFailed)}", nameof(errorCode));
65+
}
66+
}
67+
68+
/// <summary>
69+
/// Gets or sets the resource that requires authorization.
70+
/// </summary>
71+
public string? ResourceUri { get; set; }
72+
73+
/// <summary>
74+
/// Gets or sets the authorization server URI that should be used for authentication.
75+
/// </summary>
76+
public string? AuthorizationServerUri { get; set; }
77+
}

src/ModelContextProtocol/McpErrorCode.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,4 +46,20 @@ public enum McpErrorCode
4646
/// This error is used when the endpoint encounters an unexpected condition that prevents it from fulfilling the request.
4747
/// </remarks>
4848
InternalError = -32603,
49+
50+
/// <summary>
51+
/// Indicates that the client is not authorized to access the requested resource.
52+
/// </summary>
53+
/// <remarks>
54+
/// This error is returned when the client lacks the necessary credentials or permissions to access a resource.
55+
/// </remarks>
56+
Unauthorized = -32401,
57+
58+
/// <summary>
59+
/// Indicates that the authentication process failed.
60+
/// </summary>
61+
/// <remarks>
62+
/// This error is returned when the client provides invalid or expired credentials, or when the authentication flow fails.
63+
/// </remarks>
64+
AuthenticationFailed = -32402,
4965
}
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
using System.Diagnostics;
2+
3+
namespace ModelContextProtocol.Protocol.Auth;
4+
5+
/// <summary>
6+
/// Represents the context for authorization in an MCP client session.
7+
/// </summary>
8+
internal class AuthorizationContext
9+
{
10+
/// <summary>
11+
/// Gets or sets the resource metadata.
12+
/// </summary>
13+
public ResourceMetadata? ResourceMetadata { get; set; }
14+
15+
/// <summary>
16+
/// Gets or sets the authorization server metadata.
17+
/// </summary>
18+
public AuthorizationServerMetadata? AuthorizationServerMetadata { get; set; }
19+
20+
/// <summary>
21+
/// Gets or sets the client registration response.
22+
/// </summary>
23+
public ClientRegistrationResponse? ClientRegistration { get; set; }
24+
25+
/// <summary>
26+
/// Gets or sets the token response.
27+
/// </summary>
28+
public TokenResponse? TokenResponse { get; set; }
29+
30+
/// <summary>
31+
/// Gets or sets the code verifier for PKCE.
32+
/// </summary>
33+
public string? CodeVerifier { get; set; }
34+
35+
/// <summary>
36+
/// Gets or sets the redirect URI used in the authorization flow.
37+
/// </summary>
38+
public string? RedirectUri { get; set; }
39+
40+
/// <summary>
41+
/// Gets or sets the time when the access token was issued.
42+
/// </summary>
43+
public DateTimeOffset? TokenIssuedAt { get; set; }
44+
45+
/// <summary>
46+
/// Gets a value indicating whether the access token is valid.
47+
/// </summary>
48+
public bool HasValidToken => TokenResponse != null &&
49+
(TokenResponse.ExpiresIn == null ||
50+
TokenIssuedAt != null &&
51+
DateTimeOffset.UtcNow < TokenIssuedAt.Value.AddSeconds(TokenResponse.ExpiresIn.Value - 60)); // 1 minute buffer
52+
53+
/// <summary>
54+
/// Gets the access token for authentication.
55+
/// </summary>
56+
/// <returns>The access token if available, otherwise null.</returns>
57+
public string? GetAccessToken()
58+
{
59+
if (!HasValidToken)
60+
{
61+
return null;
62+
}
63+
64+
// Since HasValidToken checks that TokenResponse isn't null, we should never have null here,
65+
// but we'll add an explicit null check to satisfy the compiler
66+
return TokenResponse?.AccessToken;
67+
}
68+
69+
/// <summary>
70+
/// Gets a value indicating whether a refresh token is available for refreshing the access token.
71+
/// </summary>
72+
public bool CanRefreshToken => TokenResponse?.RefreshToken != null &&
73+
ClientRegistration != null &&
74+
AuthorizationServerMetadata != null;
75+
76+
/// <summary>
77+
/// Validates the URL of a resource against the resource URL from the metadata.
78+
/// </summary>
79+
/// <param name="resourceUrl">The URL to validate.</param>
80+
/// <returns>True if the URLs match, otherwise false.</returns>
81+
public bool ValidateResourceUrl(string resourceUrl)
82+
{
83+
if (ResourceMetadata == null || string.IsNullOrEmpty(ResourceMetadata.Resource))
84+
{
85+
return false;
86+
}
87+
88+
// Resource URL must match exactly
89+
return string.Equals(resourceUrl, ResourceMetadata.Resource, StringComparison.OrdinalIgnoreCase);
90+
}
91+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
using System.Text.Json.Serialization;
2+
3+
namespace ModelContextProtocol.Protocol.Auth;
4+
5+
/// <summary>
6+
/// Represents OAuth 2.0 authorization server metadata as defined in RFC 8414.
7+
/// </summary>
8+
internal class AuthorizationServerMetadata
9+
{
10+
/// <summary>
11+
/// Gets or sets the authorization endpoint URL.
12+
/// </summary>
13+
[JsonPropertyName("authorization_endpoint")]
14+
public required string AuthorizationEndpoint { get; set; }
15+
16+
/// <summary>
17+
/// Gets or sets the token endpoint URL.
18+
/// </summary>
19+
[JsonPropertyName("token_endpoint")]
20+
public required string TokenEndpoint { get; set; }
21+
22+
/// <summary>
23+
/// Gets or sets the client registration endpoint URL.
24+
/// </summary>
25+
[JsonPropertyName("registration_endpoint")]
26+
public string? RegistrationEndpoint { get; set; }
27+
28+
/// <summary>
29+
/// Gets or sets the token revocation endpoint URL.
30+
/// </summary>
31+
[JsonPropertyName("revocation_endpoint")]
32+
public string? RevocationEndpoint { get; set; }
33+
34+
/// <summary>
35+
/// Gets or sets the response types supported by the authorization server.
36+
/// </summary>
37+
[JsonPropertyName("response_types_supported")]
38+
public string[]? ResponseTypesSupported { get; set; } = ["code"];
39+
40+
/// <summary>
41+
/// Gets or sets the grant types supported by the authorization server.
42+
/// </summary>
43+
[JsonPropertyName("grant_types_supported")]
44+
public string[]? GrantTypesSupported { get; set; } = ["authorization_code", "refresh_token"];
45+
46+
/// <summary>
47+
/// Gets or sets the token endpoint authentication methods supported by the authorization server.
48+
/// </summary>
49+
[JsonPropertyName("token_endpoint_auth_methods_supported")]
50+
public string[]? TokenEndpointAuthMethodsSupported { get; set; } = ["client_secret_basic"];
51+
52+
/// <summary>
53+
/// Gets or sets the code challenge methods supported by the authorization server.
54+
/// </summary>
55+
[JsonPropertyName("code_challenge_methods_supported")]
56+
public string[]? CodeChallengeMethodsSupported { get; set; } = ["S256"];
57+
58+
/// <summary>
59+
/// Gets or sets the issuer identifier.
60+
/// </summary>
61+
[JsonPropertyName("issuer")]
62+
public string? Issuer { get; set; }
63+
64+
/// <summary>
65+
/// Gets or sets the scopes supported by the authorization server.
66+
/// </summary>
67+
[JsonPropertyName("scopes_supported")]
68+
public string[]? ScopesSupported { get; set; }
69+
}

0 commit comments

Comments
 (0)