-
Notifications
You must be signed in to change notification settings - Fork 265
WIP: Authorization support #349
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
c7183f6
389fb3d
ddca6cc
99a417f
53c1151
3e9462c
ecc40ab
4c7a578
d339973
bdee0e3
b0d9932
e6c1995
2f44765
bf9f63e
3fd7681
9bf4ea3
400f191
fd60a1c
f699f77
dc8f3a1
7c2e177
c88e473
4d37991
5489d65
084590d
f9f7c9d
3676c0e
950a3c4
e8b3e0d
e80bad2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -80,4 +80,7 @@ docs/api | |
|
||
# Rider | ||
.idea/ | ||
.idea_modules/ | ||
.idea_modules/ | ||
|
||
# Specs | ||
.specs/ |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
<Project Sdk="Microsoft.NET.Sdk"> | ||
|
||
<PropertyGroup> | ||
<OutputType>Exe</OutputType> | ||
<TargetFramework>net8.0</TargetFramework> | ||
<ImplicitUsings>enable</ImplicitUsings> | ||
<Nullable>enable</Nullable> | ||
</PropertyGroup> | ||
|
||
<ItemGroup> | ||
<ProjectReference Include="..\..\src\ModelContextProtocol\ModelContextProtocol.csproj" /> | ||
</ItemGroup> | ||
|
||
</Project> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,92 @@ | ||
using System.Diagnostics; | ||
using ModelContextProtocol.Client; | ||
using ModelContextProtocol.Protocol.Auth; | ||
using ModelContextProtocol.Protocol.Transport; | ||
|
||
namespace AuthorizationExample; | ||
|
||
/// <summary> | ||
/// Example demonstrating how to use the MCP C# SDK with OAuth authorization. | ||
/// </summary> | ||
public class Program | ||
{ | ||
public static async Task Main(string[] args) | ||
{ | ||
// Define the MCP server endpoint that requires OAuth authentication | ||
var serverEndpoint = new Uri("http://localhost:7071/sse"); | ||
|
||
// Configuration values for OAuth redirect | ||
string hostname = "localhost"; | ||
int port = 13261; | ||
string callbackPath = "/oauth/callback/"; | ||
|
||
// Set up the SSE transport with authorization support | ||
var transportOptions = new SseClientTransportOptions | ||
{ | ||
Endpoint = serverEndpoint, | ||
AuthorizationOptions = new AuthorizationOptions | ||
{ | ||
// Pre-registered client credentials (if applicable) | ||
ClientId = "04f79824-ab56-4511-a7cb-d7deaea92dc0", | ||
|
||
// Setting some pre-defined scopes the client requests. | ||
Scopes = ["User.Read"], | ||
|
||
// Specify the exact same redirect URIs that are registered with the OAuth server | ||
RedirectUris = new[] | ||
{ | ||
$"http://{hostname}:{port}{callbackPath}" | ||
}, | ||
// Configure the authorize callback with the same hostname, port, and path | ||
AuthorizeCallback = AuthorizationService.CreateHttpListenerAuthorizeCallback( | ||
openBrowser: async (url) => | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the url could be malicious. There has to be a more secure way to do this than blindly trusting what the server provides. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. if you hand an un‑validated string off to ShellExecute (UseShellExecute = true) you are effectively letting the user invoke any registered protocol or file association. As soon as Windows sees something like: • mshta://evil.com/boom.hta …ShellExecute will hand it off to whatever handler is registered. That means remote HTAs, LNKs that point to EXEs, custom protocols, PowerShell‐URI handlers, the Works. If all you truly want to do is open an HTTP(S) address (and nothing else), you must allowlist the scheme There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Even if this is just a sample, we shouldn't be insecure. People are likely to copy these samples into real products. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I am not sure this is really applicable here - this is the client configuration. If you're writing a MCP client, you can set the redirect URI to anything you want when you spin up the listener. Happy to discuss if you think otherwise. |
||
{ | ||
Console.WriteLine($"Opening browser to authorize at: {url}"); | ||
Process.Start(new ProcessStartInfo(url) { UseShellExecute = true }); | ||
}, | ||
hostname: hostname, | ||
listenPort: port, | ||
redirectPath: callbackPath | ||
) | ||
} | ||
}; | ||
|
||
Console.WriteLine("Connecting to MCP server..."); | ||
|
||
try | ||
{ | ||
// Create the client with authorization-enabled transport | ||
var transport = new SseClientTransport(transportOptions); | ||
var client = await McpClientFactory.CreateAsync(transport); | ||
|
||
Console.WriteLine("Successfully connected and authorized!"); | ||
|
||
// Print the list of tools available from the server. | ||
Console.WriteLine("\nAvailable tools:"); | ||
foreach (var tool in await client.ListToolsAsync()) | ||
{ | ||
Console.WriteLine($" - {tool.Name}: {tool.Description}"); | ||
} | ||
|
||
// Execute a tool (this would normally be driven by LLM tool invocations). | ||
Console.WriteLine("\nCalling 'echo' tool..."); | ||
var result = await client.CallToolAsync( | ||
"echo", | ||
new Dictionary<string, object?>() { ["message"] = "Hello MCP!" }, | ||
cancellationToken: CancellationToken.None); | ||
|
||
// echo always returns one and only one text content object | ||
Console.WriteLine($"Tool response: {result.Content.First(c => c.Type == "text").Text}"); | ||
} | ||
catch (Exception ex) | ||
{ | ||
Console.WriteLine($"Error: {ex.Message}"); | ||
if (ex.InnerException != null) | ||
{ | ||
Console.WriteLine($"Inner Error: {ex.InnerException.Message}"); | ||
} | ||
// Print the stack trace for debugging | ||
Console.WriteLine($"Stack Trace:\n{ex.StackTrace}"); | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
<Project Sdk="Microsoft.NET.Sdk.Web"> | ||
|
||
<PropertyGroup> | ||
<OutputType>Exe</OutputType> | ||
<TargetFramework>net8.0</TargetFramework> | ||
<ImplicitUsings>enable</ImplicitUsings> | ||
<Nullable>enable</Nullable> | ||
</PropertyGroup> | ||
|
||
<ItemGroup> | ||
<ProjectReference Include="..\..\src\ModelContextProtocol\ModelContextProtocol.csproj" /> | ||
<ProjectReference Include="..\..\src\ModelContextProtocol.AspNetCore\ModelContextProtocol.AspNetCore.csproj" /> | ||
</ItemGroup> | ||
|
||
</Project> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,132 @@ | ||
using ModelContextProtocol; | ||
using ModelContextProtocol.Configuration; | ||
using ModelContextProtocol.Protocol.Auth; | ||
using ModelContextProtocol.Protocol.Types; | ||
using ModelContextProtocol.Server.Auth; | ||
|
||
namespace AuthorizationServerExample; | ||
|
||
/// <summary> | ||
/// Example demonstrating how to implement authorization in an MCP server. | ||
/// </summary> | ||
public class Program | ||
{ | ||
public static async Task Main(string[] args) | ||
{ | ||
Console.WriteLine("=== MCP Server with Authorization Support ==="); | ||
Console.WriteLine("This example demonstrates how to implement OAuth authorization in an MCP server."); | ||
Console.WriteLine(); | ||
|
||
var builder = WebApplication.CreateBuilder(args); | ||
|
||
// 1. Define the Protected Resource Metadata for the server | ||
// This is the information that will be provided to clients when they need to authenticate | ||
var prm = new ProtectedResourceMetadata | ||
{ | ||
Resource = new Uri("http://localhost:7071"), // Changed from HTTPS to HTTP for local development | ||
AuthorizationServers = [ new Uri("https://login.microsoftonline.com/a2213e1c-e51e-4304-9a0d-effe57f31655/v2.0")], // Let's use a dummy Entra ID tenant here | ||
BearerMethodsSupported = ["header"], // We support the Authorization header | ||
ScopesSupported = ["mcp.tools", "mcp.prompts", "mcp.resources"], // Scopes supported by this resource | ||
ResourceDocumentation = new Uri("https://example.com/docs/mcp-server-auth") // Optional documentation URL | ||
}; | ||
|
||
// 2. Define a token validator function | ||
// This function receives the token from the Authorization header and should validate it | ||
// In a real application, this would verify the token with your identity provider | ||
async Task<bool> ValidateToken(string token) | ||
{ | ||
// For demo purposes, we'll accept any token. | ||
return true; | ||
} | ||
|
||
// 3. Create an authorization provider with the PRM and token validator | ||
var authProvider = new BasicServerAuthorizationProvider(prm, ValidateToken); // 4. Configure the MCP server with authorization | ||
// WithAuthorization will automatically configure: | ||
// - Authorization provider registration | ||
// - Protected resource metadata endpoint (/.well-known/oauth-protected-resource) | ||
// - Token validation middleware | ||
// - Authorization for all MCP endpoints | ||
builder.Services.AddMcpServer(options => | ||
{ | ||
options.ServerInstructions = "This is an MCP server with OAuth authorization enabled."; | ||
|
||
// Configure regular server capabilities like tools, prompts, resources | ||
options.Capabilities = new() | ||
{ | ||
Tools = new() | ||
{ | ||
// Simple Echo tool | ||
|
||
CallToolHandler = (request, cancellationToken) => | ||
{ | ||
if (request.Params?.Name == "echo") | ||
{ | ||
if (request.Params.Arguments?.TryGetValue("message", out var message) is not true) | ||
{ | ||
throw new McpException("Missing required argument 'message'"); | ||
} | ||
|
||
return new ValueTask<CallToolResponse>(new CallToolResponse() | ||
{ | ||
Content = [new Content() { Text = $"Echo: {message}", Type = "text" }] | ||
}); | ||
} | ||
|
||
// Protected tool that requires authorization | ||
if (request.Params?.Name == "protected-data") | ||
{ | ||
// This tool will only be accessible to authenticated clients | ||
return new ValueTask<CallToolResponse>(new CallToolResponse() | ||
{ | ||
Content = [new Content() { Text = "This is protected data that only authorized clients can access" }] | ||
}); | ||
} | ||
|
||
throw new McpException($"Unknown tool: '{request.Params?.Name}'"); | ||
}, | ||
|
||
ListToolsHandler = async (_, _) => new() | ||
{ | ||
Tools = | ||
[ | ||
new() | ||
{ | ||
Name = "echo", | ||
Description = "Echoes back the message you send" | ||
}, | ||
new() | ||
{ | ||
Name = "protected-data", | ||
Description = "Returns protected data that requires authorization" | ||
} | ||
] | ||
} | ||
} | ||
}; | ||
}) | ||
.WithAuthorization(authProvider) // Enable authorization with our provider | ||
.WithHttpTransport(); // Configure HTTP transport | ||
|
||
var app = builder.Build(); | ||
|
||
// 5. Map MCP endpoints | ||
// Note: Authorization is now handled automatically by WithAuthorization() | ||
app.MapMcp(); | ||
|
||
// Configure the server URL | ||
app.Urls.Add("http://localhost:7071"); | ||
|
||
Console.WriteLine("Starting MCP server with authorization at http://localhost:7071"); | ||
Console.WriteLine("PRM Document URL: http://localhost:7071/.well-known/oauth-protected-resource"); | ||
|
||
Console.WriteLine(); | ||
Console.WriteLine("To test the server:"); | ||
Console.WriteLine("1. Use an MCP client that supports authorization"); | ||
Console.WriteLine("2. When prompted for authorization, enter 'valid_token' to gain access"); | ||
Console.WriteLine("3. Any other token value will be rejected with a 401 Unauthorized"); | ||
Console.WriteLine(); | ||
Console.WriteLine("Press Ctrl+C to stop the server"); | ||
|
||
await app.RunAsync(); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
{ | ||
"profiles": { | ||
"AuthorizationServerExample": { | ||
"commandName": "Project", | ||
"launchBrowser": true, | ||
"environmentVariables": { | ||
"ASPNETCORE_ENVIRONMENT": "Development" | ||
}, | ||
"applicationUrl": "https://localhost:50481;http://localhost:50482" | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
using Microsoft.AspNetCore.Builder; | ||
|
||
namespace ModelContextProtocol.AspNetCore; | ||
|
||
/// <summary> | ||
/// Extension methods for using MCP authorization in ASP.NET Core applications. | ||
/// </summary> | ||
public static class AuthorizationExtensions | ||
{ | ||
/// <summary> | ||
/// Adds MCP authorization middleware to the specified <see cref="IApplicationBuilder"/>, which enables | ||
/// OAuth 2.0 authorization for MCP servers. | ||
/// | ||
/// Note: This method is called automatically when using <c>WithAuthorization()</c>, so you typically | ||
/// don't need to call it directly. It's available for advanced scenarios where more control is needed. | ||
/// </summary> | ||
/// <param name="builder">The <see cref="IApplicationBuilder"/> to add the middleware to.</param> | ||
/// <returns>A reference to this instance after the operation has completed.</returns> | ||
public static IApplicationBuilder UseMcpAuthorization(this IApplicationBuilder builder) | ||
{ | ||
return builder.UseMiddleware<AuthorizationMiddleware>(); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have a usecase for this to be dynamic. I haven't looked at the rest of the code yet, but please ensure there is support for supplying the scopes dynamically in the server metadata based upon the incoming request.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@DavidParks8 - this is an example. You can specify whatever scopes you want here when you establish the transport options.