Skip to content

Dynamic Authorization in Streamable HTTP Client #700

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 8 commits into
base: main
Choose a base branch
from

Conversation

aravind-segu
Copy link

@aravind-segu aravind-segu commented May 12, 2025

Motivation and Context

As described in #652, the streamable HTTP client currently can be configured to include a static dictionary of headers with each request, including an Authorization header for authentication. However, to support authenticating to an MCP server using short-lived access tokens, the client needs the ability to obtain a fresh token across requests to the MCP Server. This PR introduces an AuthClientProvider base class to support custom authentication and adds the ability to pass in an instance of AuthClientProvider when instantiating a StreamableHTTPClient. If specified, the headers returned from auth_client_provider.get_headers() are passed along to each request to the Server. This resembles the setup in the TypeScript SDK.

In a follow-up PR, we'll complete parity with the TypeScript SDK & enhance the interface to support Oauth 2.0 flows, so that developers need only implement key pieces of the Oauth 2.0 authorization code flow rather than a full-fledged get_headers implementation, while still preserving the ability to override get_headers to support authenticating to an MCP server from environments where an oauth token or other auth is already available

How Has This Been Tested?

Unit Tests
For e2e test, built an example implementation of an interface against a OAuth compliant server

import asyncio
from datetime import datetime, timedelta
from typing import Optional

from mcp.client.session import ClientSession
from mcp.client.streamable_http import streamablehttp_client, AuthClientProvider

class CustomAuthClientProvider(AuthClientProvider):
    def __init__(self):
        # Initialize Auth Provider

    async def get_headers(self) -> str:
        # Implement getting dynamic headers

# Auth Provider specified here
auth_client_provider = CustomAuthClientProvider()

async def query_mcp():
    async with streamablehttp_client(
        url="<URL>",
        auth_client_provider=auth_client_provider,
    ) as (read_stream, write_stream, _):
        async with ClientSession(read_stream, write_stream) as session:
            await session.initialize()
            tools = await session.list_tools()
            print(tools)


async def main():
    await query_mcp()


if __name__ == "__main__":
    asyncio.run(main())

Breaking Changes

No

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

result = await session.initialize()
assert isinstance(result, InitializeResult)

# Make multiple requests to verify token updates

Choose a reason for hiding this comment

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

Dumb question, where do we verify the token is actually updated?

Copy link
Author

Choose a reason for hiding this comment

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

It is really hard in this testing environment to get the headers and verify the implementation. There is a mock server which hosts a list and set tools. We create a session, and add our messages to the write stream. This is then read by our transport layer and a request is sent to the server. I could not find a way to intercept or inspect this request object to verify the headers. So I just ensured the method was being called

Copy link
Author

Choose a reason for hiding this comment

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

We could try to build a custom app, that looks at the header, then calls the server, and returns the auth headers in the response headers. I will wait for the maintainer to chime in if they have better ideas on how to test this.

@@ -102,6 +115,7 @@ def __init__(
CONTENT_TYPE: JSON,
**self.headers,
Copy link

@smurching smurching May 12, 2025

Choose a reason for hiding this comment

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

BTW I would expect any auth headers passed in self.headers to take precedence over the auth_provider headers - in many APIs explicitly setting a value (e.g. explicitly setting the value of the Authorization header via headers) takes precedence over implicitly inferring it. For example, I've seen that pattern often in MLflow. But will defer to the maintainers' opinion on that - in either case, it'd be good to have a test that exercises the behavior

Copy link
Author

Choose a reason for hiding this comment

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

Good call, I added the behaviour to not override passed in headers, and add a test case as well

Copy link

@smurching smurching left a comment

Choose a reason for hiding this comment

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

Don't have permissions to approve, but this looks good to me, thanks @aravind-segu !

@aravind-segu aravind-segu changed the title Dynamic Token Authorization in Streamable HTTP Client Dynamic Authorization in Streamable HTTP Client May 13, 2025
@ihrpr ihrpr added this to the 2025-03-26 spec release milestone May 14, 2025
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