Thanks to visit codestin.com
Credit goes to github.com

Skip to content

feature(auth): Allow delegating OAuth authorization to existing app-level implementations #485

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

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

m-paternostro
Copy link
Contributor

@m-paternostro m-paternostro commented May 13, 2025

An optional method that clients can use whenever the authorization should be delegated to an existing implementation.

This PR introduces a new optional method delegateAuthorization to the OAuthClientProvider interface. It allows clients to short-circuit the standard OAuth flow when they already manage authorization through another mechanism (e.g. platform tokens, ambient credentials, preexisting identity systems). When implemented, this method gives control back to the host application to determine whether it considers the session authorized - if so, the SDK skips its internal flow entirely.

Motivation and Context

Some applications embedding the MCP SDK already have fully functional authorization systems. In such cases, the SDK’s built-in OAuth flow can be redundant or even problematic - especially when the app simply needs to know when authorization is required, not how to perform it.

Prior to this change, the only way to hook into the authorization process was by subclassing StreamableHTTPClientTransport and/or SSEClientTransport and overriding enough methods to reimplement _authThenStart. However, because the relevant methods are private and deeply interwoven (e.g. send, _startOrAuthSse), doing so required replicating a significant amount of transport code - leading to maintenance burden and fragile overrides.

This change introduces a clean, focused mechanism for opting into externally-managed auth without needing to reimplement large portions of the transport logic.

How Has This Been Tested?

The design was validated by subclassing SSEClientTransport and making the necessary changes to use this new hook.

Breaking Changes

No: the new method is purely opt-in, backward-compatible, and safely ignored if unimplemented. It’s designed to be as simple and low-friction as possible while avoiding the need to subclass transports or bypass internal behavior.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Checklist

  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling
  • I have added or updated documentation as needed

Additional context

Notes about the changes:

  • The auth function in src/client/auth.ts now checks for delegateAuthorization (if provided) before entering the standard flow.
  • Comprehensive unit tests were added to verify:
    • The hook is invoked when present.
    • Returning "AUTHORIZED" short-circuits the flow.
    • Returning undefined falls back to the built-in behavior.
    • The hook is not called when authorizationCode is already present.
  • The implementation follows existing conventions, including how auth.ts handles errors and fallback behavior when attempting refresh or token exchange.

@m-paternostro m-paternostro force-pushed the mp/delegatedauth branch 5 times, most recently from 3352fce to 96f19fc Compare May 15, 2025 13:03
@m-paternostro m-paternostro changed the title feature(auth): OAuthClientProvider.delegateAuthorization feature(auth): Allow delegating OAuth authorization to existing app-level implementations May 15, 2025
@m-paternostro m-paternostro force-pushed the mp/delegatedauth branch 4 times, most recently from b0e2654 to 1bf3a74 Compare May 21, 2025 03:02
@ihrpr ihrpr added this to the HPR milestone May 21, 2025
@m-paternostro m-paternostro force-pushed the mp/delegatedauth branch 4 times, most recently from 7758221 to 02f8659 Compare May 27, 2025 14:06
@m-paternostro m-paternostro force-pushed the mp/delegatedauth branch 2 times, most recently from 59b8e7f to e544126 Compare May 30, 2025 11:54
@m-paternostro
Copy link
Contributor Author

Hi @ihrpr ,

Sorry for the direct tag. I really appreciate that this is already on the HPR list, and I completely understand you have a lot on your plate.

This is just a gentle nudge on the PR - a decision here would really help me move forward on my side, especially if the change is accepted.

Thanks again for all the amazing work on the SDK! It’s been a real pleasure working with it over the past three months ;-)

@m-paternostro m-paternostro force-pushed the mp/delegatedauth branch 2 times, most recently from b8b1a68 to 96d2b31 Compare June 10, 2025 19:29
@m-paternostro
Copy link
Contributor Author

One additional detail worth mentioning: the OAuth implementation used by my company (and others as well 😉) includes JWT tokens in the final authentication response. These tokens encode valuable metadata such as user identity, organization context, and more.

This is yet another reason to allow client implementations to fully control the authentication flow - they may want to extract and act on this information in ways that are specific to their environment.

@m-paternostro m-paternostro force-pushed the mp/delegatedauth branch 7 times, most recently from aa7a7b1 to 2be7d47 Compare June 20, 2025 12:19
@m-paternostro
Copy link
Contributor Author

👋 Hello,

I just wanted to follow up on this PR. I’ve been keeping it up to date over the past month, including adapting to changes like the recent addition of protected resource support (RFC 8707), which this PR now explicitly handles.

I really appreciate that it was marked as an HPR and I took that as a sign that it might be reviewed soon. I also sent a (hopefully gentle) nudge to @ihrpr at the time, just to make sure it was on the radar.

My main reason for commenting now is to ask for a bit of clarity: I’m more than happy to continue monitoring and updating this PR for as long as there’s a reasonable chance it might be reviewed and potentially merged. I sincerely believe it improves the SDK’s OAuth2 support and brings tangible value to the community.

That said, I totally understand if this PR isn’t likely to be accepted either for technical or strategic reasons. However, in this case, I’d rather step back than keep chasing changes unnecessarily.

Regardless, thanks again for all the work you do on this project!

Copy link
Contributor

@pcarleton pcarleton left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👋 hi @m-paternostro thanks for the PR, and sorry for the delay.

Would you be able to provide an example of how you'd use this in practice? Some code snippets would be great. I'm having difficulty contextualizing this, e.g.:

  • what would you use the metadata for in the function?
  • would it be better to delegate the whole auth function and not discover any metadata first? e.g. leave it up to the implementor to decide if they need metadata
  • what parts of the existing flow break down for you?
  • is the auth flow you're supporting still OAuth or something else?

If there are specific parts of the OAuthProvider that you can override instead of the whole auth flow, I'd prefer that, or if it makes sense to override at a higher level that's something to consider. The control flow in the auth function is fairly complex as it is, and this adds another branch to consider that I'd like to do only if absolutely necessary.

@ihrpr ihrpr modified the milestones: HPR, auth Jun 25, 2025
@m-paternostro
Copy link
Contributor Author

m-paternostro commented Jun 25, 2025

Hi @pcarleton, thanks for reaching out and for the thoughtful questions!

I will try to answer all of them, sharing context on why this change has been so valuable in our case and potentially in other environments too. Let me know if I missed something or if you’d like to explore alternative designs.

What would you use the metadata for in the function?

To be honest, my current implementation doesn’t use the metadata. I included it in the patch because it’s readily available at that point where I'm injecting the delegate call into the flow, and I figured others might benefit from it. It could be useful for:

  • Logging/auditing (e.g. knowing which AS or resource was selected before delegation)
  • Routing logic for multi-tenant environments
  • Dynamic fallback or context-based decision-making based on metadata or resource hints

Would it be better to delegate the whole auth function and not discover any metadata first?

That’s definitely simpler to implement, but I leaned toward a more integrated design to allow delegation with full context. This also helps preserve compatibility with RFC 8707 (resource indicators), future metadata-driven decisions, or scenarios where delegation might only apply for certain resources or servers.

That said, I’m 100% okay if there is a preference for a leaner version that calls delegateAuthorization(...) right at the top - I just didn’t want to shortcut the flow prematurely or assume all delegation would be unconditional.

What parts of the existing flow break down for you?

The flow itself is solid - the issue is more about not duplicating it and/or existing implementations.

In my case, the host app (a VS Code extension, now targeting Cursor) already has:

  • A production-ready, OAuth-compliant implementation that handles redirect flows, VS Code UX (and quirks), nonce validation, token refresh, etc.
  • Tokens that embed organization/user metadata, which we parse for UX, telemetry, and similar logic
  • A stable session system that has been in use for over 2 years

So instead of trying to reimplement all of that again inside the MCP auth abstraction, it made more sense to delegate - basically, share tokens, trap a 401, do a refresh using our existing code, then try again.

I did try to wire our system into the SDK’s OAuth machinery directly, but it was hard to do without forking or patching significant parts our OAuth flow implementation - so, at this moment, our code ships with a modified copy of the Streamable and SSE transports that are aware the proposed delegateAuthorization method. This PR is my way of reducing that footprint back down to pretty much a single-line hook.

Is the auth flow you’re supporting still OAuth or something else?

It’s absolutely still OAuth - fully standards-compliant - but orchestrated by the host app (the extension in my case). We use an authorization server, token endpoints, and scopes, and we persist tokens in our own session model (which uses VS Code APIs to write to the machine's keychain). With the proposed change, the SDK just needs to ask for tokens and react to 401s - the rest is handled by our host environment.

Also, our tokens embed extra fields (e.g. user/org identifiers) that are JWT encoded - we extract those post-auth for custom logic and, afaik, other companies do something similar.

These kinds of extensions are hard to model generically, and honestly, I wouldn’t expect the SDK to try (at least at this moment). And that’s another reason I think having a lean “take control” hook is valuable: it lets people plug in existing solutions.

The control flow in the auth function is fairly complex

Indeed! And, honestly, kudos to the team for implementing it.

My first idea was to try to inject the hooks into the currently flow but it quickly became a very complicated mess, with way more code that I thought is necessary for this feature, and, even then, would probably not cover all possible corners that everybody out there need.

Perhaps naively, I assume that taking control over the flow probably applies to more than just my case, being useful for mobile apps, thin clients, or any environment that already manages sessions and just needs to connect to MCP with minimal overhead.

In my case, the extension runs inside Cursor, which, like other environments, doesn’t currently expose any way for extensions to plug into auth-related flows directly. In environments like this, SDK-level delegation isn’t just a convenience - it is arguably the only path that does not require huge rewrites or design compromises.

this adds another branch to consider that I'd like to do only if absolutely necessary.

I tried to keep the implementation minimal and low-risk and this was one the reasons to fork the default flow right at the beginning instead of intermixing hooks all over the place, which would make hard to implement future changes. Also, if delegateAuthorization returns undefined, everything proceeds as it does today, which I'd hope means zero behavior change unless explicitly used.

That said, if there’s a cleaner or more idiomatic injection point I’ve overlooked, I’d be happy to adapt! I have to confess that, as of SDK 1.13, this was the most self-contained way I found to implement the feature with minimal changes to the code base - that's one of the reasons I didn't go for changes on the transport classes.


Thanks again for taking the time to look through this!

I am happy to adjust the approach if there’s a simpler way forward. Just hoping we can find a design that supports host-authenticated environments without requiring them to reinvent the entire stack.

Let me know what you think!

@m-paternostro m-paternostro force-pushed the mp/delegatedauth branch 2 times, most recently from dd35f24 to 9d0539e Compare June 25, 2025 21:46
@pcarleton pcarleton self-assigned this Jun 26, 2025
@pcarleton
Copy link
Contributor

Hi @m-paternostro thanks for providing more context.

Are you able to share a snippet of what your: delegateAuthorization implementation looks like in your client? I believe you that the current API's and available overrides don't suit your needs, but I'm still not getting the specifics.

Also, is this a client targeting a single MCP server? or is it meant to be used for any user provided MCP server?

The overall message I'm getting is "I've already got a function that does the auth flow i need, please let me uses it, and don't make me re-write it / re-test it etc. just to fit the more generic flow that I don't need." Is that right?

Some priorities in my mind are:

  • Compatibility
  • Security
  • API surface maintainability

In general we want MCP clients and servers to be compatible with each other as much as is reasonable. For security, standardized auth flows means its easier to get fixes deployed more quickly if there are fewer implementations of the same critical code. And for API surface maintainability, we want to be able to quickly reason about whether a given change is breaking or not.

Weighing all those together, and if my understanding above is correct (pending the snippet etc.) I'd prefer we delegate at a higher level. That makes this function easier to reason about, and introduces fewer cases where odd interactions might happen. If we add a parameter, or change metadata discovery, I don't want to have to decide whether that comes above or below the delegation call. Similarly, it should be clear it's fully up to the delegating implementer to implement the security best practices, it shouldn't be a mix of responsibility.

In terms of a higher level, this could mean:

  • (simplest, but messy) Move the delegate call above the metadata discovery
    • What I don't love about this is that you've got this OAuthProvider with all these functions required on it, but you're overriding the whole flow that uses it, so you don't actually need any of those functions aside from tokens() to be called in the transport.
  • Add a "401-handler" optional parameter to the transports, so you can intercept there, and have the whole response object available

I'm leaning towards 401 interception as being a more complete solution to this issue, wdyt?

@m-paternostro
Copy link
Contributor Author

m-paternostro commented Jun 26, 2025

thanks for providing more context.

Really my pleasure. Thank you for your time!

Are you able to share a snippet of what your: delegateAuthorization implementation looks like in your client?

I forgot you asked this on your first comment. Sorry.

Here you are:

const authProvider = createDelegatedOAuthClientProvider(
  // tokens
  () => {
    const scope = this.checkScope();

    const accessToken = scope.accessToken();
    if (accessToken) {
      scope.logger.i`injecting access token into the backend transport`;
      return { token_type: 'Bearer', access_token: accessToken };
    }

    scope.logger.d`no access token found to inject into the backend transport`;
    return undefined;
  },

  // delegateAuthorization
  async () => {
    const scope = this.checkScope();

    scope.logger.i`authenticating for the backend transport`;
    const response = await scope
      .tryFetchToRefreshToken()
      .then(() => true)
      .catch((error: unknown) => {
        scope.logger.args(error).e`backend authentication failed: ${error}`;
        return false;
      });
    return response ? 'AUTHORIZED' : undefined;
  },
);

is this a client targeting a single MCP server?

This is being used when targeting MCP Servers that are available on an already authenticated backend.

is it meant to be used for any user provided MCP server?

Not at the moment - for now we only do this for very specific servers.

The overall message I'm getting is... Is that right?

Pretty much ;-)

I would just rephrase the "re-write it" to emphasize the goal of not duplicating it, and ending up with "similar but slightly different" implementations, one for the extension and another for MCP server.

Btw, besides the code duplication, a more pressing issue for us would be to ensure that both flows are always in sync - without going into too many details, our users need to switch between "organizations" and that's imply managing different live OAuth sessions.

Some priorities in my mind are: ...

Nice list.

I'd prefer we delegate at a higher level.
it should be clear it's fully up to the delegating implementer to implement the security best practices, it shouldn't be a mix of responsibility.

All points make total sense.

In terms of a higher level, this could mean:
(simplest, but messy)...

I certainly agree with moving the call to delegateAuthorization to the top.

Some ideas regarding the other concerns...

  • About the metadata computation, perhaps the delegateAuthorization method could take the exact arguments passed to auth (minus the provider). That reinforces the idea of total delegation.

  • About the need to provide an implementation for the required methods in OAuthProvider:

    • If you like the idea that the delegation can "choose" to let the normal flow happen, I'd suggest keeping it as is.
    • If you prefer the approach in which the delegation is a "no return" choice, perhaps the type of the provider ultimately passed to the auth method could be OAuthClientProvider | DelegatedOAuthClientProvider, with the latter exposing just the tokens and the refresh method. This would require changing the transports.

In terms of a higher level, this could mean:
Add a "401-handler"...

Hmmm... Not a bad call although I see 3 potential issues:

  • The access token would still need to be injected. Maybe the handler should be an object with 2 methods (one for the 401 and another for the tokens)

  • The hook would need to be added to every transport (present and future).

  • The code for both Streamable and SSE transports have multiple places checking for 401. The hook would probably need to be invoked in all (or most) of them, which may complicate the overall flow.


Let me know your thoughts. I can update the code based on whatever is decided here.

An optional method that clients can use whenever the authorization should be delegated to an existing implementation.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants