Skip to content

Commit ac75230

Browse files
Varun SharmaCopilot
andcommitted
Add IncludeResourceIndicator option for OAuth and DI client sample
Fix #648: Add IncludeResourceIndicator option to ClientOAuthOptions to allow suppressing the RFC 8707 'resource' parameter in OAuth requests. This enables compatibility with OAuth providers like MS Entra-ID (Azure AD v2.0) that do not support the resource parameter. Fix #147: Add DependencyInjectionClient sample project demonstrating how to wire up MCP clients with Microsoft.Extensions.DependencyInjection and IHostedService. Add DI guidance section to getting-started docs. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 94aa695 commit ac75230

File tree

7 files changed

+183
-2
lines changed

7 files changed

+183
-2
lines changed

ModelContextProtocol.slnx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
<Project Path="samples/AspNetCoreMcpPerSessionTools/AspNetCoreMcpPerSessionTools.csproj" />
4343
<Project Path="samples/AspNetCoreMcpServer/AspNetCoreMcpServer.csproj" />
4444
<Project Path="samples/ChatWithTools/ChatWithTools.csproj" />
45+
<Project Path="samples/DependencyInjectionClient/DependencyInjectionClient.csproj" />
4546
<Project Path="samples/EverythingServer/EverythingServer.csproj" />
4647
<Project Path="samples/InMemoryTransport/InMemoryTransport.csproj" />
4748
<Project Path="samples/LongRunningTasks/LongRunningTasks.csproj" />

docs/concepts/getting-started.md

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,52 @@ Console.WriteLine(result.Content.OfType<TextContentBlock>().First().Text);
133133

134134
Clients can connect to any MCP server, not just ones created with this library. The protocol is server-agnostic.
135135

136+
#### Using dependency injection with MCP clients
137+
138+
To use an MCP client in an application with dependency injection, register it as a singleton service and consume it from hosted services or other DI-managed components:
139+
140+
```csharp
141+
using Microsoft.Extensions.DependencyInjection;
142+
using Microsoft.Extensions.Hosting;
143+
using Microsoft.Extensions.Logging;
144+
using ModelContextProtocol.Client;
145+
146+
var builder = Host.CreateApplicationBuilder(args);
147+
148+
builder.Services.AddSingleton(sp =>
149+
{
150+
var loggerFactory = sp.GetRequiredService<ILoggerFactory>();
151+
var transport = new StdioClientTransport(new()
152+
{
153+
Name = "My Server",
154+
Command = "dotnet",
155+
Arguments = ["run", "--project", "path/to/server"],
156+
});
157+
return McpClient.CreateAsync(transport, loggerFactory: loggerFactory)
158+
.GetAwaiter().GetResult();
159+
});
160+
161+
builder.Services.AddHostedService<MyWorker>();
162+
await builder.Build().RunAsync();
163+
```
164+
165+
For Docker-based MCP servers, configure the transport with `docker run`:
166+
167+
```csharp
168+
var transport = new StdioClientTransport(new()
169+
{
170+
Name = "my-mcp-server",
171+
Command = "docker",
172+
Arguments = ["run", "-i", "--rm", "mcp/my-server"],
173+
EnvironmentVariables = new Dictionary<string, string>
174+
{
175+
["API_KEY"] = configuration["ApiKey"]!,
176+
},
177+
});
178+
```
179+
180+
See the [`DependencyInjectionClient`](https://github.com/modelcontextprotocol/csharp-sdk/tree/main/samples/DependencyInjectionClient) sample for a complete example.
181+
136182
#### Using tools with an LLM
137183

138184
`McpClientTool` inherits from `AIFunction`, so the tools returned by `ListToolsAsync` can be handed directly to any `IChatClient`:
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
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.Core\ModelContextProtocol.Core.csproj" />
12+
</ItemGroup>
13+
14+
<ItemGroup>
15+
<PackageReference Include="Microsoft.Extensions.Hosting" />
16+
</ItemGroup>
17+
18+
</Project>
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
using Microsoft.Extensions.DependencyInjection;
2+
using Microsoft.Extensions.Hosting;
3+
using Microsoft.Extensions.Logging;
4+
using ModelContextProtocol.Client;
5+
using ModelContextProtocol.Protocol;
6+
7+
// This sample demonstrates how to wire up MCP clients with dependency injection
8+
// using Microsoft.Extensions.Hosting and IServiceCollection.
9+
10+
var builder = Host.CreateApplicationBuilder(args);
11+
12+
// Register an MCP client as a singleton.
13+
// The factory method creates the client with a StdioClientTransport.
14+
// Replace the command/arguments with your own MCP server (e.g., a Docker container).
15+
builder.Services.AddSingleton(sp =>
16+
{
17+
var loggerFactory = sp.GetRequiredService<ILoggerFactory>();
18+
19+
var transport = new StdioClientTransport(new()
20+
{
21+
Name = "Everything",
22+
Command = "npx",
23+
Arguments = ["-y", "@modelcontextprotocol/server-everything"],
24+
});
25+
26+
// McpClient.CreateAsync is async; we block here for DI registration.
27+
// In production, consider using an IHostedService to initialize async resources.
28+
return McpClient.CreateAsync(transport, loggerFactory: loggerFactory)
29+
.GetAwaiter().GetResult();
30+
});
31+
32+
// Register a hosted service that uses the MCP client.
33+
builder.Services.AddHostedService<McpWorker>();
34+
35+
var host = builder.Build();
36+
await host.RunAsync();
37+
38+
/// <summary>
39+
/// A background service that demonstrates using an injected MCP client.
40+
/// </summary>
41+
sealed class McpWorker(McpClient mcpClient, ILogger<McpWorker> logger, IHostApplicationLifetime lifetime) : BackgroundService
42+
{
43+
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
44+
{
45+
// List available tools from the MCP server.
46+
var tools = await mcpClient.ListToolsAsync(cancellationToken: stoppingToken);
47+
logger.LogInformation("Available tools ({Count}):", tools.Count);
48+
foreach (var tool in tools)
49+
{
50+
logger.LogInformation(" {Name}: {Description}", tool.Name, tool.Description);
51+
}
52+
53+
// Invoke a tool.
54+
var result = await mcpClient.CallToolAsync(
55+
"echo",
56+
new Dictionary<string, object?> { ["message"] = "Hello from DI!" },
57+
cancellationToken: stoppingToken);
58+
59+
var text = result.Content.OfType<TextContentBlock>().FirstOrDefault()?.Text;
60+
logger.LogInformation("Echo result: {Result}", text);
61+
62+
// Shut down after the demo completes.
63+
lifetime.StopApplication();
64+
}
65+
}

src/ModelContextProtocol.Core/Authentication/ClientOAuthOptions.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,4 +103,17 @@ public sealed class ClientOAuthOptions
103103
/// If none is provided, tokens will be cached with the transport.
104104
/// </summary>
105105
public ITokenCache? TokenCache { get; set; }
106+
107+
/// <summary>
108+
/// Gets or sets a value indicating whether to include the <c>resource</c> parameter in OAuth authorization
109+
/// and token requests as defined by <see href="https://datatracker.ietf.org/doc/rfc8707/">RFC 8707</see>.
110+
/// </summary>
111+
/// <remarks>
112+
/// <para>
113+
/// The default value is <see langword="true"/>. Set to <see langword="false"/> when using an OAuth provider
114+
/// that does not support the <c>resource</c> parameter, such as Microsoft Entra ID (Azure AD v2.0),
115+
/// which returns error <c>AADSTS901002</c> when the parameter is present.
116+
/// </para>
117+
/// </remarks>
118+
public bool IncludeResourceIndicator { get; set; } = true;
106119
}

src/ModelContextProtocol.Core/Authentication/ClientOAuthProvider.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ internal sealed partial class ClientOAuthProvider : McpHttpClient
4141

4242
private readonly HttpClient _httpClient;
4343
private readonly ILogger _logger;
44+
private readonly bool _includeResourceIndicator;
4445

4546
private string? _clientId;
4647
private string? _clientSecret;
@@ -90,6 +91,7 @@ public ClientOAuthProvider(
9091
_dcrInitialAccessToken = options.DynamicClientRegistration?.InitialAccessToken;
9192
_dcrResponseDelegate = options.DynamicClientRegistration?.ResponseDelegate;
9293
_tokenCache = options.TokenCache ?? new InMemoryTokenCache();
94+
_includeResourceIndicator = options.IncludeResourceIndicator;
9395
}
9496

9597
/// <summary>
@@ -709,8 +711,8 @@ private async Task PerformDynamicClientRegistrationAsync(
709711
}
710712
}
711713

712-
private static string? GetResourceUri(ProtectedResourceMetadata protectedResourceMetadata)
713-
=> protectedResourceMetadata.Resource;
714+
private string? GetResourceUri(ProtectedResourceMetadata protectedResourceMetadata)
715+
=> _includeResourceIndicator ? protectedResourceMetadata.Resource : null;
714716

715717
private string? GetScopeParameter(ProtectedResourceMetadata protectedResourceMetadata)
716718
{

tests/ModelContextProtocol.AspNetCore.Tests/OAuth/AuthTests.cs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1261,4 +1261,40 @@ public async Task CanAuthenticate_WithLegacyServerUsingDefaultEndpointFallback()
12611261
await using var client = await McpClient.CreateAsync(
12621262
transport, loggerFactory: LoggerFactory, cancellationToken: TestContext.Current.CancellationToken);
12631263
}
1264+
1265+
[Fact]
1266+
public async Task CanAuthenticate_WithoutResourceIndicator()
1267+
{
1268+
await using var app = await StartMcpServerAsync();
1269+
1270+
Uri? capturedAuthorizationUrl = null;
1271+
1272+
await using var transport = new HttpClientTransport(new()
1273+
{
1274+
Endpoint = new(McpServerUrl),
1275+
OAuth = new()
1276+
{
1277+
ClientId = "demo-client",
1278+
ClientSecret = "demo-secret",
1279+
RedirectUri = new Uri("http://localhost:1179/callback"),
1280+
IncludeResourceIndicator = false,
1281+
AuthorizationRedirectDelegate = (authorizationUri, redirectUri, cancellationToken) =>
1282+
{
1283+
capturedAuthorizationUrl = authorizationUri;
1284+
// Return null to signal that authorization was not completed.
1285+
return Task.FromResult<string?>(null);
1286+
},
1287+
},
1288+
}, HttpClient, LoggerFactory);
1289+
1290+
// The auth flow will fail because we return null from the delegate,
1291+
// but we only need to verify the authorization URL was constructed correctly.
1292+
await Assert.ThrowsAsync<McpException>(() => McpClient.CreateAsync(
1293+
transport, loggerFactory: LoggerFactory, cancellationToken: TestContext.Current.CancellationToken));
1294+
1295+
Assert.NotNull(capturedAuthorizationUrl);
1296+
var query = QueryHelpers.ParseQuery(capturedAuthorizationUrl.Query);
1297+
Assert.False(query.ContainsKey("resource"), "The 'resource' query parameter should not be present when IncludeResourceIndicator is false.");
1298+
Assert.True(query.ContainsKey("scope"), "The 'scope' query parameter should still be present.");
1299+
}
12641300
}

0 commit comments

Comments
 (0)