Skip to content

IDownstreamApi / ITokenAcquisition unusable inside MCP Tool over HTTP transport (HttpContext == null, user scopes lost) #590

@nkarg90

Description

@nkarg90

Describe the bug
When an MCP Tool is executed through the HTTP transport, the ASP‑NET HttpContext is null inside the tool’s method. Because of this, dependency‑injected services that depend on the current context (IDownstreamApi, ITokenAcquisition, IHttpContextAccessor) cannot resolve the caller’s identity or scopes.
As a result, it is impossible to call a downstream API on behalf of the user (OBO flow) from inside the tool—even though the MCP server itself was reached with a valid bearer token from VS Code.

Expected behavior
Inside a tool method I should be able to call:

await downstreamApiService.CallApiForUserAsync("DownstreamApi1", …)
await downstreamApiService.CallApiForUserAsync("DownstreamApi2", …)

or

var token = await tokenAcquisition.GetAccessTokenForUserAsync(scopes);

and receive a token that represents the same authenticated user who connected to the MCP server, preserving the original authentication identity (on behalf of 'OBO').

Actual behavior
HttpContext is null inside the tool’s DI scope, so:

  • IDownstreamApi.CallApiForUserAsync throws InvalidOperationException (Cannot find user assertion …).
  • ITokenAcquisition.GetAccessTokenForUserAsync cannot locate the incoming token.
  • Injecting IHttpContextAccessor also returns null.

This effectively breaks any tool that needs to forward the user’s identity to another Entra ID–protected API.

Minimal steps to reproduce

  1. Configure Startup code
var builder = WebApplication.CreateBuilder(args);

builder.AddServiceDefaults();

builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAd"))
    .EnableTokenAcquisitionToCallDownstreamApi()
    .AddDownstreamApi("DownstreamApi1", builder.Configuration.GetSection("DownstreamApi1"))
    .AddDownstreamApi("DownstreamApi2", builder.Configuration.GetSection("DownstreamApi2"))
    .AddInMemoryTokenCaches();

builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("AccessAsUserScope", policy =>
    {
        policy.RequireAuthenticatedUser();
        policy.RequireClaim(ClaimConstants.Scope, "access_as_user");
    });
});

builder.Services
    .AddMcpServer()
    .WithHttpTransport()
    .WithToolsFromAssembly();

var app = builder.Build();

app.UseAuthentication();
app.UseAuthorization();

app.MapMcp("/mcp").RequireAuthorization("AccessAsUserScope");
app.Run();
  1. Configure MCPServerTools Code
[McpServerToolType]
public class CallOrganizationApisTool
{
    [McpServerTool]
    public async Task<string> GetAccounts([FromServices] IDownstreamApi downstreamApiService)
    {
        var resp = await downstreamApiService.CallApiForUserAsync(
            "DownstreamApi1",
            options => options.RelativePath = "accounts");

        return await resp.Content.ReadAsStringAsync();
    }

    [McpServerTool]
    public async Task<string> GetUsers([FromServices] IDownstreamApi downstreamApiService)
    {
        var resp = await downstreamApiService.CallApiForUserAsync(
            "DownstreamApi2",
            options => options.RelativePath = "users");

        return await resp.Content.ReadAsStringAsync();
    }

    [McpServerTool]
    public static async Task<string> GetInterfaces([FromServices] ITokenAcquisition tokenAcquisition)
    {
        var scopes = new[]
        {
            "api://client-id/access_as_user"
        };

        var accessToken =
            await tokenAcquisition.GetAccessTokenForUserAsync(scopes);

        using var http = new HttpClient();
        http.DefaultRequestHeaders.Authorization =
            new AuthenticationHeaderValue("Bearer", accessToken);

        return await http.GetStringAsync(
            $"https://yourapi/interfaces");
    }
}
  1. Launch the server and connect from VS Code with an mcp.json that includes a bearer token obtained via powershell az account get-access-token or postman (oauth 2.0).

  2. Invoke either tool method → exception / empty HttpContext.

Workarounds tried

  • Injecting IHttpContextAccessor → returns null.
  • Manually passing the incoming token to the tool via parameters → breaks the tool contract (OBO flow).
  • Created a scoped service and injected it via middleware between app.UseAuthentication() and app.UseAuthorization() in order to cache the incoming user's access token. However, when injecting that scoped service into the tool, the tool receives a separate DI scope and all the values set by the middleware are lost—again due to the tool running in an isolated context.
  • Enabling Stateless = true in HttpServerTransportOptions makes HttpContext available, which solves the issue—but this comes with major trade-offs, including:
    • sampling and roots capabilities are disabled
    • /sse endpoint becomes unavailable
    • no support for unsolicited server-to-client messages

Many organizations rely on custom HTTP headers to pass sensitive credentials (such as API keys) or to include metadata that downstream services require to perform specific actions. This functionality is essential in many real-world scenarios. Being able to access these headers during tool execution would allow developers to preserve and forward them when making additional API calls from within the tool.

Possible cause
WithHttpTransport() appears to spin up a separate DI scope for tool execution without propagating the current HttpContext (or its User). That strips the request of its original scopes, making OBO impossible.

Requested fix / proposal

  • Propagate the invoking HttpContext (or at least ClaimsPrincipal + access token) into the tool’s service provider when the transport is HTTP.
  • Alternatively, provide an official extension point or guidance to enable OBO from within a tool without requiring stateless mode.
  • Expose a per‑tool scope callback—similar to RunSessionHandler, but executed at tool‑scope level.
    • The callback would receive the current HttpContext (or at least the relevant ClaimsPrincipal and access token) so that custom services can be registered or configured before the tool method runs—for example, capturing custom HTTP headers, API keys, or user tokens that need to be forwarded in downstream calls.

Final notes
Calling downstream APIs on behalf of the user (OBO) is a very common practice—especially in modern systems that rely on chained API integrations across organizational boundaries. Being able to use IDownstreamApi or ITokenAcquisition inside a tool would enable developers to cleanly delegate calls without breaking encapsulation or authentication flow.

We strongly believe supporting OBO scenarios inside MCP tools would unlock many real-world use cases.

Finally, I’d like to thank the Anthropic & .NET team for the incredible work and dedication put into building the MCP library for C# and .NET. The SDK is shaping up to be a very powerful foundation for modern developer tools, and we're excited to see it evolve!

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions