Skip to content

Commit 8c790db

Browse files
committed
Tweaks to logic
1 parent 4b3f9f7 commit 8c790db

File tree

11 files changed

+239
-164
lines changed

11 files changed

+239
-164
lines changed

samples/SecureWeatherClient/Program.cs

Lines changed: 32 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ namespace SecureWeatherClient;
1010
class Program
1111
{
1212
// The URI for our OAuth redirect - in a real app, this would be a registered URI or a local server
13-
private static readonly Uri RedirectUri = new("http://localhost:8888/oauth-callback");
13+
private static readonly Uri RedirectUri = new("http://localhost:1170/oauth-callback");
1414

1515
static async Task Main(string[] args)
1616
{
@@ -20,32 +20,47 @@ static async Task Main(string[] args)
2020

2121
// Create an HTTP client with OAuth handling
2222
var oauthHandler = new OAuthDelegatingHandler(
23+
clientId: "04f79824-ab56-4511-a7cb-d7deaea92dc0",
2324
redirectUri: RedirectUri,
24-
clientName: "SecureWeatherClient",
25-
scopes: new[] { "weather.read" },
26-
authorizationHandler: HandleAuthorizationRequestAsync);
25+
clientName: "SecureWeatherClient",
26+
scopes: ["weather.read"],
27+
authorizationHandler: HandleAuthorizationRequestAsync)
28+
{
29+
// The OAuth handler needs an inner handler
30+
InnerHandler = new HttpClientHandler()
31+
};
2732

2833
var httpClient = new HttpClient(oauthHandler);
29-
var serverUrl = "http://localhost:5000"; // Default server URL
30-
34+
var serverUrl = "http://localhost:55598/sse"; // Default server URL
35+
3136
// Allow the user to specify a different server URL
3237
Console.WriteLine($"Server URL (press Enter for default: {serverUrl}):");
3338
var userInput = Console.ReadLine();
3439
if (!string.IsNullOrWhiteSpace(userInput))
3540
{
3641
serverUrl = userInput;
3742
}
38-
43+
3944
Console.WriteLine();
4045
Console.WriteLine($"Connecting to weather server at {serverUrl}...");
41-
42-
// Create an MCP client with the server URL
43-
var client = new McpClient(new Uri(serverUrl), httpClient);
4446

4547
try
4648
{
49+
// Create SseClientTransportOptions with the server URL
50+
var transportOptions = new SseClientTransportOptions
51+
{
52+
Endpoint = new Uri(serverUrl),
53+
Name = "Secure Weather Client"
54+
};
55+
56+
// Create SseClientTransport with our authenticated HTTP client
57+
var transport = new SseClientTransport(transportOptions, httpClient);
58+
59+
// Create an MCP client using the factory method with our transport
60+
var client = await McpClientFactory.CreateAsync(transport);
61+
4762
// Get the list of available tools
48-
var tools = await client.GetToolsAsync();
63+
var tools = await client.ListToolsAsync();
4964
if (tools.Count == 0)
5065
{
5166
Console.WriteLine("No tools available on the server.");
@@ -54,55 +69,14 @@ static async Task Main(string[] args)
5469

5570
Console.WriteLine($"Found {tools.Count} tools on the server.");
5671
Console.WriteLine();
57-
58-
// Find the weather tool
59-
var weatherTool = tools.FirstOrDefault(t => t.Name == "get_weather");
60-
if (weatherTool == null)
61-
{
62-
Console.WriteLine("The server does not provide a weather tool.");
63-
return;
64-
}
65-
66-
// Get the weather for different locations
67-
string[] locations = { "New York", "London", "Tokyo", "Sydney", "Moscow" };
68-
69-
foreach (var location in locations)
70-
{
71-
try
72-
{
73-
Console.WriteLine($"Getting weather for {location}...");
74-
var result = await client.InvokeToolAsync(weatherTool.Name, new Dictionary<string, object>
75-
{
76-
["location"] = location
77-
});
78-
79-
if (result.TryGetValue("temperature", out var temperature) &&
80-
result.TryGetValue("conditions", out var conditions) &&
81-
result.TryGetValue("humidity", out var humidity) &&
82-
result.TryGetValue("windSpeed", out var windSpeed))
83-
{
84-
Console.WriteLine($"Weather in {location}:");
85-
Console.WriteLine($" Temperature: {temperature}°C");
86-
Console.WriteLine($" Conditions: {conditions}");
87-
Console.WriteLine($" Humidity: {humidity}%");
88-
Console.WriteLine($" Wind speed: {windSpeed} km/h");
89-
}
90-
else
91-
{
92-
Console.WriteLine($"Invalid response format for {location}");
93-
}
94-
}
95-
catch (Exception ex)
96-
{
97-
Console.WriteLine($"Error getting weather for {location}: {ex.Message}");
98-
}
99-
100-
Console.WriteLine();
101-
}
10272
}
10373
catch (Exception ex)
10474
{
10575
Console.WriteLine($"Error: {ex.Message}");
76+
if (ex.InnerException != null)
77+
{
78+
Console.WriteLine($"Inner error: {ex.InnerException.Message}");
79+
}
10680
}
10781

10882
Console.WriteLine("Press any key to exit...");
@@ -124,13 +98,13 @@ private static Task<string> HandleAuthorizationRequestAsync(Uri authorizationUri
12498
Console.WriteLine();
12599
Console.WriteLine("After authentication, you will be redirected to a page with a code.");
126100
Console.WriteLine("Please enter the code parameter from the URL:");
127-
101+
128102
var authorizationCode = Console.ReadLine();
129103
if (string.IsNullOrWhiteSpace(authorizationCode))
130104
{
131105
throw new InvalidOperationException("Authorization code is required.");
132106
}
133-
107+
134108
return Task.FromResult(authorizationCode);
135109
}
136110
}
Lines changed: 118 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,132 @@
1-
using SecureWeatherServer.Tools;
2-
using System.Net.Http.Headers;
1+
using ModelContextProtocol.AspNetCore;
2+
using ModelContextProtocol.Protocol.Types;
3+
using Microsoft.AspNetCore.Authentication.JwtBearer;
34

45
var builder = WebApplication.CreateBuilder(args);
56

6-
builder.Services.AddMcpServer()
7-
.WithHttpTransport()
8-
.WithTools<WeatherTools>()
9-
.WithAuthorization(metadata =>
7+
// Configure MCP Server
8+
builder.Services.AddMcpServer(options =>
9+
{
10+
options.ServerInstructions = "This is an MCP server with OAuth authorization enabled.";
11+
12+
// Configure regular server capabilities like tools, prompts, resources
13+
options.Capabilities = new()
14+
{
15+
Tools = new()
16+
{
17+
// Simple Echo tool
18+
CallToolHandler = (request, cancellationToken) =>
19+
{
20+
if (request.Params?.Name == "echo")
21+
{
22+
if (request.Params.Arguments?.TryGetValue("message", out var message) is not true)
23+
{
24+
throw new Exception("It happens.");
25+
}
26+
27+
return new ValueTask<CallToolResponse>(new CallToolResponse()
28+
{
29+
Content = [new Content() { Text = $"Echo: {message}", Type = "text" }]
30+
});
31+
}
32+
33+
// Protected tool that requires authorization
34+
if (request.Params?.Name == "protected-data")
35+
{
36+
// This tool will only be accessible to authenticated clients
37+
return new ValueTask<CallToolResponse>(new CallToolResponse()
38+
{
39+
Content = [new Content() { Text = "This is protected data that only authorized clients can access" }]
40+
});
41+
}
42+
43+
throw new Exception("It happens.");
44+
},
45+
46+
ListToolsHandler = async (_, _) => new()
47+
{
48+
Tools =
49+
[
50+
new()
51+
{
52+
Name = "echo",
53+
Description = "Echoes back the message you send"
54+
},
55+
new()
56+
{
57+
Name = "protected-data",
58+
Description = "Returns protected data that requires authorization"
59+
}
60+
]
61+
}
62+
}
63+
};
64+
})
65+
.WithHttpTransport()
66+
.WithAuthorization(metadata =>
67+
{
68+
// Configure the OAuth metadata for this server
69+
metadata.AuthorizationServers.Add(new Uri("https://auth.example.com"));
70+
71+
// Define the scopes this server supports
72+
metadata.ScopesSupported.AddRange(["weather.read", "weather.write"]);
73+
74+
// Add optional documentation
75+
metadata.ResourceDocumentation = new Uri("https://docs.example.com/api/weather");
76+
});
77+
78+
// Configure authentication
79+
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
80+
.AddJwtBearer(options =>
1081
{
11-
metadata.AuthorizationServers.Add(new Uri("https://auth.example.com"));
12-
metadata.ScopesSupported.AddRange(["weather.read", "weather.write"]);
13-
metadata.ResourceDocumentation = new Uri("https://docs.example.com/api/weather");
82+
// In a real app, you would configure proper JWT validation
83+
options.Events = new JwtBearerEvents
84+
{
85+
OnMessageReceived = context =>
86+
{
87+
// Simple demo authentication - in a real app, use proper JWT validation
88+
var token = context.Request.Headers.Authorization.ToString().Replace("Bearer ", "");
89+
if (token == "valid_token")
90+
{
91+
// For demo purposes, simulate successful auth with a valid token
92+
context.Success();
93+
}
94+
return Task.CompletedTask;
95+
}
96+
};
1497
});
1598

16-
builder.Services.AddSingleton(_ =>
99+
// Add authorization policy for MCP
100+
builder.Services.AddAuthorization(options =>
17101
{
18-
var client = new HttpClient() { BaseAddress = new Uri("https://api.weather.gov") };
19-
client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("weather-tool", "1.0"));
20-
return client;
102+
options.AddPolicy("McpAuth", policy =>
103+
{
104+
policy.RequireAuthenticatedUser();
105+
policy.RequireClaim("scope", "weather.read");
106+
});
21107
});
22108

23109
var app = builder.Build();
24110

25-
app.UseCors(policy => policy
26-
.AllowAnyOrigin()
27-
.AllowAnyMethod()
28-
.AllowAnyHeader());
29-
111+
// Set up the middleware pipeline
30112
app.UseAuthentication();
31113
app.UseAuthorization();
32114

33-
app.Run();
115+
// Map MCP endpoints with authorization
116+
app.MapMcp();
117+
118+
// Configure the server URL
119+
app.Urls.Add("http://localhost:7071");
120+
121+
Console.WriteLine("Starting MCP server with authorization at http://localhost:7071");
122+
Console.WriteLine("PRM Document URL: http://localhost:7071/.well-known/oauth-protected-resource");
123+
124+
Console.WriteLine();
125+
Console.WriteLine("To test the server:");
126+
Console.WriteLine("1. Use an MCP client that supports authorization");
127+
Console.WriteLine("2. When prompted for authorization, enter 'valid_token' to gain access");
128+
Console.WriteLine("3. Any other token value will be rejected with a 401 Unauthorized");
129+
Console.WriteLine();
130+
Console.WriteLine("Press Ctrl+C to stop the server");
131+
132+
await app.RunAsync();

samples/SecureWeatherServer/SecureWeatherServer.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
<Project Sdk="Microsoft.NET.Sdk.Web">
33

44
<PropertyGroup>
5-
<TargetFramework>net8.0</TargetFramework>
5+
<TargetFramework>net9.0</TargetFramework>
66
<Nullable>enable</Nullable>
77
<ImplicitUsings>enable</ImplicitUsings>
88
</PropertyGroup>
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
namespace ModelContextProtocol.Auth;
2+
3+
/// <summary>
4+
/// Configuration options for the authorization code flow.
5+
/// </summary>
6+
public class AuthorizationCodeOptions
7+
{
8+
/// <summary>
9+
/// The client ID.
10+
/// </summary>
11+
public string ClientId { get; set; } = string.Empty;
12+
13+
/// <summary>
14+
/// The client secret.
15+
/// </summary>
16+
public string? ClientSecret { get; set; }
17+
18+
/// <summary>
19+
/// The redirect URI.
20+
/// </summary>
21+
public Uri RedirectUri { get; set; } = null!;
22+
23+
/// <summary>
24+
/// The authorization endpoint.
25+
/// </summary>
26+
public Uri AuthorizationEndpoint { get; set; } = null!;
27+
28+
/// <summary>
29+
/// The token endpoint.
30+
/// </summary>
31+
public Uri TokenEndpoint { get; set; } = null!;
32+
33+
/// <summary>
34+
/// The scope to request.
35+
/// </summary>
36+
public string? Scope { get; set; }
37+
38+
/// <summary>
39+
/// PKCE values for the authorization flow.
40+
/// </summary>
41+
public PkceUtility.PkceValues PkceValues { get; set; } = null!;
42+
43+
/// <summary>
44+
/// A state value to protect against CSRF attacks.
45+
/// </summary>
46+
public string State { get; set; } = string.Empty;
47+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
namespace ModelContextProtocol.Auth;
2+
3+
/// <summary>
4+
/// Configuration for OAuth authorization.
5+
/// </summary>
6+
public class AuthorizationConfig
7+
{
8+
/// <summary>
9+
/// The URI to redirect to after authentication.
10+
/// </summary>
11+
public Uri RedirectUri { get; set; } = null!;
12+
13+
/// <summary>
14+
/// The client ID to use for authentication, or null to register a new client.
15+
/// </summary>
16+
public string? ClientId { get; set; }
17+
18+
/// <summary>
19+
/// The client name to use for registration.
20+
/// </summary>
21+
public string? ClientName { get; set; }
22+
23+
/// <summary>
24+
/// The requested scopes.
25+
/// </summary>
26+
public IEnumerable<string>? Scopes { get; set; }
27+
28+
/// <summary>
29+
/// The handler to invoke when authorization is required.
30+
/// </summary>
31+
public Func<Uri, Task<string>>? AuthorizationHandler { get; set; }
32+
}

0 commit comments

Comments
 (0)