Skip to content

Commit 53c1151

Browse files
committed
Stub for server implementation
1 parent 99a417f commit 53c1151

File tree

15 files changed

+535
-18
lines changed

15 files changed

+535
-18
lines changed

ModelContextProtocol.sln

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ModelContextProtocol.AspNet
5858
EndProject
5959
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AuthorizationExample", "samples\AuthorizationExample\AuthorizationExample.csproj", "{C2E8E0D9-5F7B-38D8-3D5D-041471BD350C}"
6060
EndProject
61+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AuthorizationServerExample", "samples\AuthorizationServerExample\AuthorizationServerExample.csproj", "{05C500AF-9CF6-C2E7-2782-95271975A5DE}"
62+
EndProject
6163
Global
6264
GlobalSection(SolutionConfigurationPlatforms) = preSolution
6365
Debug|Any CPU = Debug|Any CPU
@@ -116,6 +118,10 @@ Global
116118
{C2E8E0D9-5F7B-38D8-3D5D-041471BD350C}.Debug|Any CPU.Build.0 = Debug|Any CPU
117119
{C2E8E0D9-5F7B-38D8-3D5D-041471BD350C}.Release|Any CPU.ActiveCfg = Release|Any CPU
118120
{C2E8E0D9-5F7B-38D8-3D5D-041471BD350C}.Release|Any CPU.Build.0 = Release|Any CPU
121+
{05C500AF-9CF6-C2E7-2782-95271975A5DE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
122+
{05C500AF-9CF6-C2E7-2782-95271975A5DE}.Debug|Any CPU.Build.0 = Debug|Any CPU
123+
{05C500AF-9CF6-C2E7-2782-95271975A5DE}.Release|Any CPU.ActiveCfg = Release|Any CPU
124+
{05C500AF-9CF6-C2E7-2782-95271975A5DE}.Release|Any CPU.Build.0 = Release|Any CPU
119125
EndGlobalSection
120126
GlobalSection(SolutionProperties) = preSolution
121127
HideSolutionNode = FALSE
@@ -135,6 +141,7 @@ Global
135141
{37B6A5E0-9995-497D-8B43-3BC6870CC716} = {A2F1F52A-9107-4BF8-8C3F-2F6670E7D0AD}
136142
{85557BA6-3D29-4C95-A646-2A972B1C2F25} = {2A77AF5C-138A-4EBB-9A13-9205DCD67928}
137143
{C2E8E0D9-5F7B-38D8-3D5D-041471BD350C} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
144+
{05C500AF-9CF6-C2E7-2782-95271975A5DE} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
138145
EndGlobalSection
139146
GlobalSection(ExtensibilityGlobals) = postSolution
140147
SolutionGuid = {384A3888-751F-4D75-9AE5-587330582D89}

samples/AuthorizationExample/Program.cs

Lines changed: 16 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -27,25 +27,23 @@ public static async Task Main(string[] args)
2727
)
2828
};
2929

30-
try
31-
{
32-
// Create the client with authorization-enabled transport
33-
var transport = new SseClientTransport(transportOptions);
34-
var client = await McpClientFactory.CreateAsync(transport);
30+
// Create the client with authorization-enabled transport
31+
var transport = new SseClientTransport(transportOptions);
32+
var client = await McpClientFactory.CreateAsync(transport);
3533

36-
// Print the list of tools available from the server.
37-
foreach (var tool in await client.ListToolsAsync())
38-
{
39-
Console.WriteLine($"{tool.Name} ({tool.Description})");
40-
}
34+
// Print the list of tools available from the server.
35+
foreach (var tool in await client.ListToolsAsync())
36+
{
37+
Console.WriteLine($"{tool.Name} ({tool.Description})");
38+
}
4139

42-
// Execute a tool (this would normally be driven by LLM tool invocations).
43-
var result = await client.CallToolAsync(
44-
"echo",
45-
new Dictionary<string, object?>() { ["message"] = "Hello MCP!" },
46-
cancellationToken: CancellationToken.None);
40+
// Execute a tool (this would normally be driven by LLM tool invocations).
41+
var result = await client.CallToolAsync(
42+
"echo",
43+
new Dictionary<string, object?>() { ["message"] = "Hello MCP!" },
44+
cancellationToken: CancellationToken.None);
4745

48-
// echo always returns one and only one text content object
49-
Console.WriteLine(result.Content.First(c => c.Type == "text").Text);
50-
}
46+
// echo always returns one and only one text content object
47+
Console.WriteLine(result.Content.First(c => c.Type == "text").Text);
48+
}
5149
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<Project Sdk="Microsoft.NET.Sdk.Web">
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+
<ProjectReference Include="..\..\src\ModelContextProtocol.AspNetCore\ModelContextProtocol.AspNetCore.csproj" />
13+
</ItemGroup>
14+
15+
</Project>
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
using Microsoft.AspNetCore.Builder;
2+
using ModelContextProtocol;
3+
using ModelContextProtocol.AspNetCore;
4+
using ModelContextProtocol.Protocol.Auth;
5+
using ModelContextProtocol.Protocol.Types;
6+
using ModelContextProtocol.Server.Auth;
7+
using System.Text.Json;
8+
9+
namespace AuthorizationServerExample;
10+
11+
/// <summary>
12+
/// Example demonstrating how to implement authorization in an MCP server.
13+
/// </summary>
14+
public class Program
15+
{
16+
public static async Task Main(string[] args)
17+
{
18+
Console.WriteLine("=== MCP Server with Authorization Support ===");
19+
Console.WriteLine("This example demonstrates how to implement OAuth authorization in an MCP server.");
20+
Console.WriteLine();
21+
22+
var builder = WebApplication.CreateBuilder(args);
23+
24+
// 1. Define the Protected Resource Metadata for the server
25+
// This is the information that will be provided to clients when they need to authenticate
26+
var prm = new ProtectedResourceMetadata
27+
{
28+
Resource = "https://localhost:7071", // The resource identifier (typically your server's base URL)
29+
AuthorizationServers = ["https://auth.example.com"], // Auth servers that can issue tokens for this resource
30+
BearerMethodsSupported = ["header"], // We support the Authorization header
31+
ScopesSupported = ["mcp.tools", "mcp.prompts", "mcp.resources"], // Scopes supported by this resource
32+
ResourceDocumentation = "https://example.com/docs/mcp-server-auth" // Optional documentation URL
33+
};
34+
35+
// 2. Define a token validator function
36+
// This function receives the token from the Authorization header and should validate it
37+
// In a real application, this would verify the token with your identity provider
38+
async Task<bool> ValidateToken(string token)
39+
{
40+
// For demo purposes, we'll accept any token that starts with "valid_"
41+
// In production, you would validate the token with your identity provider
42+
var isValid = token.StartsWith("valid_", StringComparison.OrdinalIgnoreCase);
43+
Console.WriteLine($"Token validation result: {(isValid ? "Valid" : "Invalid")}");
44+
return isValid;
45+
}
46+
47+
// 3. Create an authorization provider with the PRM and token validator
48+
var authProvider = new SimpleServerAuthorizationProvider(prm, ValidateToken);
49+
50+
// 4. Configure the MCP server with authorization
51+
builder.Services.AddMcpServer(options =>
52+
{
53+
options.ServerInstructions = "This is an MCP server with OAuth authorization enabled.";
54+
55+
// Configure regular server capabilities like tools, prompts, resources
56+
options.Capabilities = new()
57+
{
58+
Tools = new()
59+
{
60+
// Simple Echo tool
61+
62+
CallToolHandler = (request, cancellationToken) =>
63+
{
64+
if (request.Params?.Name == "echo")
65+
{
66+
if (request.Params.Arguments?.TryGetValue("message", out var message) is not true)
67+
{
68+
throw new McpException("Missing required argument 'message'");
69+
}
70+
71+
return new ValueTask<CallToolResponse>(new CallToolResponse()
72+
{
73+
Content = [new Content() { Text = $"Echo: {message}", Type = "text" }]
74+
});
75+
}
76+
77+
// Protected tool that requires authorization
78+
if (request.Params?.Name == "protected-data")
79+
{
80+
// This tool will only be accessible to authenticated clients
81+
return new ValueTask<CallToolResponse>(new CallToolResponse()
82+
{
83+
Content = [new Content() { Text = "This is protected data that only authorized clients can access" }]
84+
});
85+
}
86+
87+
throw new McpException($"Unknown tool: '{request.Params?.Name}'");
88+
},
89+
90+
ListToolsHandler = async (_, _) => new()
91+
{
92+
Tools =
93+
[
94+
new()
95+
{
96+
Name = "echo",
97+
Description = "Echoes back the message you send"
98+
},
99+
new()
100+
{
101+
Name = "protected-data",
102+
Description = "Returns protected data that requires authorization"
103+
}
104+
]
105+
}
106+
}
107+
};
108+
})
109+
.WithAuthorization(authProvider) // Enable authorization with our provider
110+
.WithHttpTransport(); // Configure HTTP transport
111+
112+
var app = builder.Build();
113+
114+
// 5. Enable authorization middleware (this must be before MapMcp)
115+
// This middleware does several things:
116+
// - Serves the PRM document at /.well-known/oauth-protected-resource
117+
// - Checks Authorization header on requests
118+
// - Returns 401 + WWW-Authenticate when authorization is missing or invalid
119+
app.UseMcpAuthorization();
120+
121+
// 6. Map MCP endpoints
122+
app.MapMcp();
123+
124+
// Configure the server URL
125+
app.Urls.Add("https://localhost:7071");
126+
127+
Console.WriteLine("Starting MCP server with authorization at https://localhost:7071");
128+
Console.WriteLine("PRM Document URL: https://localhost:7071/.well-known/oauth-protected-resource");
129+
Console.WriteLine();
130+
Console.WriteLine("To test the server:");
131+
Console.WriteLine("1. Use an MCP client that supports authorization");
132+
Console.WriteLine("2. When prompted for authorization, enter 'valid_token' to gain access");
133+
Console.WriteLine("3. Any other token value will be rejected with a 401 Unauthorized");
134+
Console.WriteLine();
135+
Console.WriteLine("Press Ctrl+C to stop the server");
136+
137+
await app.RunAsync();
138+
}
139+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"profiles": {
3+
"AuthorizationServerExample": {
4+
"commandName": "Project",
5+
"launchBrowser": true,
6+
"environmentVariables": {
7+
"ASPNETCORE_ENVIRONMENT": "Development"
8+
},
9+
"applicationUrl": "https://localhost:50481;http://localhost:50482"
10+
}
11+
}
12+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
using Microsoft.AspNetCore.Builder;
2+
using Microsoft.Extensions.DependencyInjection;
3+
4+
namespace ModelContextProtocol.AspNetCore;
5+
6+
/// <summary>
7+
/// Extension methods for using MCP authorization in ASP.NET Core applications.
8+
/// </summary>
9+
public static class McpAuthorizationExtensions
10+
{
11+
/// <summary>
12+
/// Adds MCP authorization middleware to the specified <see cref="IApplicationBuilder"/>, which enables
13+
/// OAuth 2.0 authorization for MCP servers.
14+
/// </summary>
15+
/// <param name="builder">The <see cref="IApplicationBuilder"/> to add the middleware to.</param>
16+
/// <returns>A reference to this instance after the operation has completed.</returns>
17+
public static IApplicationBuilder UseMcpAuthorization(this IApplicationBuilder builder)
18+
{
19+
return builder.UseMiddleware<McpAuthorizationMiddleware>();
20+
}
21+
}
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
using Microsoft.AspNetCore.Http;
2+
using Microsoft.Extensions.Logging;
3+
using Microsoft.Extensions.Options;
4+
using ModelContextProtocol.Protocol.Auth;
5+
using ModelContextProtocol.Protocol.Types;
6+
using ModelContextProtocol.Server;
7+
using ModelContextProtocol.Utils.Json;
8+
using System.Text.Json;
9+
10+
namespace ModelContextProtocol.AspNetCore;
11+
12+
/// <summary>
13+
/// Middleware that handles authorization for MCP servers.
14+
/// </summary>
15+
internal class McpAuthorizationMiddleware
16+
{
17+
private readonly RequestDelegate _next;
18+
private readonly ILogger<McpAuthorizationMiddleware> _logger;
19+
20+
/// <summary>
21+
/// Initializes a new instance of the <see cref="McpAuthorizationMiddleware"/> class.
22+
/// </summary>
23+
/// <param name="next">The next middleware in the pipeline.</param>
24+
/// <param name="logger">The logger factory.</param>
25+
public McpAuthorizationMiddleware(RequestDelegate next, ILogger<McpAuthorizationMiddleware> logger)
26+
{
27+
_next = next ?? throw new ArgumentNullException(nameof(next));
28+
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
29+
}
30+
31+
/// <summary>
32+
/// Processes a request.
33+
/// </summary>
34+
/// <param name="context">The HTTP context.</param>
35+
/// <param name="serverOptions">The MCP server options.</param>
36+
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
37+
public async Task InvokeAsync(HttpContext context, IOptions<McpServerOptions> serverOptions)
38+
{
39+
// Check if authorization is configured
40+
var authCapability = serverOptions.Value.Capabilities?.Authorization;
41+
var authProvider = authCapability?.AuthorizationProvider;
42+
43+
if (authProvider == null)
44+
{
45+
// Authorization is not configured, proceed to the next middleware
46+
await _next(context);
47+
return;
48+
}
49+
50+
// Handle the PRM document endpoint
51+
if (context.Request.Path.StartsWithSegments("/.well-known/oauth-protected-resource"))
52+
{
53+
_logger.LogDebug("Serving Protected Resource Metadata document");
54+
context.Response.ContentType = "application/json";
55+
await JsonSerializer.SerializeAsync(
56+
context.Response.Body,
57+
authProvider.GetProtectedResourceMetadata(),
58+
McpJsonUtilities.DefaultOptions.GetTypeInfo(typeof(ProtectedResourceMetadata)));
59+
return;
60+
}
61+
62+
// Serve SSE and message endpoints with authorization
63+
if (context.Request.Path.StartsWithSegments("/sse") ||
64+
(context.Request.Path.Value?.EndsWith("/message") == true))
65+
{
66+
// Check if the Authorization header is present
67+
if (!context.Request.Headers.TryGetValue("Authorization", out var authHeader) || string.IsNullOrEmpty(authHeader))
68+
{
69+
// No Authorization header present, return 401 Unauthorized
70+
var prm = authProvider.GetProtectedResourceMetadata();
71+
var prmUrl = GetPrmUrl(context, prm.Resource);
72+
73+
_logger.LogDebug("Authorization required, returning 401 Unauthorized with WWW-Authenticate header");
74+
context.Response.StatusCode = StatusCodes.Status401Unauthorized;
75+
context.Response.Headers.Append("WWW-Authenticate", $"Bearer resource_metadata=\"{prmUrl}\"");
76+
return;
77+
}
78+
79+
// Validate the token - ensuring authHeader is a non-null string
80+
string authHeaderValue = authHeader.ToString();
81+
bool isValid = await authProvider.ValidateTokenAsync(authHeaderValue);
82+
if (!isValid)
83+
{
84+
// Invalid token, return 401 Unauthorized
85+
var prm = authProvider.GetProtectedResourceMetadata();
86+
var prmUrl = GetPrmUrl(context, prm.Resource);
87+
88+
_logger.LogDebug("Invalid authorization token, returning 401 Unauthorized");
89+
context.Response.StatusCode = StatusCodes.Status401Unauthorized;
90+
context.Response.Headers.Append("WWW-Authenticate", $"Bearer resource_metadata=\"{prmUrl}\"");
91+
return;
92+
}
93+
}
94+
95+
// Token is valid or endpoint doesn't require authentication, proceed to the next middleware
96+
await _next(context);
97+
}
98+
99+
private static string GetPrmUrl(HttpContext context, string resourceUri)
100+
{
101+
// Use the actual resource URI from PRM if it's an absolute URL, otherwise build the URL
102+
if (Uri.TryCreate(resourceUri, UriKind.Absolute, out _))
103+
{
104+
return $"{resourceUri.TrimEnd('/')}/.well-known/oauth-protected-resource";
105+
}
106+
107+
// Build the URL from the current request
108+
var request = context.Request;
109+
var scheme = request.Scheme;
110+
var host = request.Host.Value;
111+
return $"{scheme}://{host}/.well-known/oauth-protected-resource";
112+
}
113+
}

0 commit comments

Comments
 (0)