Skip to content

Conversation

amithkk
Copy link

@amithkk amithkk commented Sep 6, 2025

This change introduces comprehensive authentication capabilities to MCPAdapt, enabling connections to MCP servers that require authentication. The implementation supports three authentication methods: OAuth 2.0, API Key, and Bearer Token authentication. Closes #44

The OAuth 2.0 implementation includes a complete authorization code flow with support for dynamic client registration. The LocalBrowserOAuthHandler provides a browser-based authentication experience, while the BaseOAuthHandler abstract class allows developers to create custom handlers for specialized environments like headless servers or CLI applications. Token storage is handled through a extendible interface with an included InMemoryTokenStorage implementation. Base classes from the official MCP library have been used and re-exported wherever it makes sense

The authentication system integrates transparently with the existing MCPAdapt core through the auth_provider parameter in the MCPAdapt constructor. It supports multiple MCP servers with different authentication methods and works across SSE, and streamable-http.

Error handling includes specific exception types for different OAuth failure scenarios, providing clear feedback for timeout, cancellation, network, and configuration errors. The implementation includes comprehensive test coverage and working examples, including an example integration with the Canva MCP server.

Documentation covers all authentication methods with practical examples, custom handler creation guides, and security best practices.

Coverage info:

Name                                   Stmts   Miss  Cover   Missing
--------------------------------------------------------------------
src/mcpadapt/__init__.py                   1      0   100%
src/mcpadapt/auth/__init__.py              7      0   100%
src/mcpadapt/auth/exceptions.py           41      0   100%
src/mcpadapt/auth/handlers.py            127      5    96%   96, 201, 229, 241, 271
src/mcpadapt/auth/oauth.py                27      0   100%
src/mcpadapt/auth/providers.py            21      0   100%
src/mcpadapt/core.py                     138     23    83%   50, 72, 112, 126-130, 136, 138, 296, 392-432
src/mcpadapt/crewai_adapter.py            42     10    76%   76-77, 80-84, 108, 112-121
src/mcpadapt/google_genai_adapter.py      20      7    65%   83-89, 104-118
src/mcpadapt/langchain_adapter.py         87     21    76%   50, 91-99, 115, 226, 239-260
src/mcpadapt/smolagents_adapter.py       101     23    77%   49, 118-122, 153-156, 164, 177-182, 195, 214, 227, 247, 251-270
src/mcpadapt/utils/__init__.py             0      0   100%
src/mcpadapt/utils/modeling.py           100     22    78%   54, 94-106, 122, 124-133, 183-184
tests/auth/conftest.py                    54      8    85%   12, 87, 97, 103-107
tests/auth/test_core_auth.py             253      2    99%   15, 22
tests/auth/test_exceptions.py            174      0   100%
tests/auth/test_handlers.py              340      0   100%
tests/auth/test_oauth.py                 285      0   100%
tests/auth/test_providers.py             214      5    98%   24, 27, 228, 387, 390
tests/test_core.py                       136      1    99%   358
tests/test_crewai_adapter.py             105      3    97%   17, 25-26
tests/test_google_genai_adapter.py        52      0   100%
tests/test_langchain_adapter.py           70      0   100%
tests/test_smolagents_adapter.py         110      0   100%
tests/utils/test_modeling.py               8      0   100%
--------------------------------------------------------------------
TOTAL                                   2513    130    95%

@amithkk amithkk force-pushed the feat/auth branch 2 times, most recently from 82d5bda to cb3a9c0 Compare September 6, 2025 22:22
@amithkk
Copy link
Author

amithkk commented Sep 9, 2025

@grll This is a relatively large PR so feel free to let me know if you'd like me to split it up in any way, and I am happy to help maintain the auth code over a longer period of time if you'd like 😄

@greedy52
Copy link

Love this capability. From our use case, we would like to use BearerAuthProvider. However, we need to a way to refresh the token when it expires. It would be nice for BearerAuthProvider to be able to take a function or interface that can retrieve the token instead of hard coding it during construction.

@amithkk
Copy link
Author

amithkk commented Sep 13, 2025

@greedy52 - That seems like a valuable addition, considering that you'd need it to retrieve tokens from externally managed auth that expire quicker than the length of the agent "session". Now added with docs!

@ukimme-work
Copy link

This works great under smolagents for our internal use!

Is this being actively reviewed? We'd much rather not have to maintain a fork for this functionality.
Are there any plans to add support for integrating with things like Streamlit or Chainlit for the auth flow?

@amithkk
Copy link
Author

amithkk commented Sep 14, 2025

@ukimme-work Great to hear!

Although integrating it with something like Streamlit/Chainlit would be neat for quickly setting up a working UI for using agents with MCP, I'd think it'd be out of scope for this specific pull request which focuses on adding the core features for Auth. Feel free to create an issue with details on how you see that coming together.

@ukimme-work
Copy link

@amithkk ok, will do.

@grll could you please look at reviewing this? 🙏

Copy link
Owner

Choose a reason for hiding this comment

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

In the spirit of minimizing our footprint (mcpadapt is more or less a wrapper around the python sdk client), I think we should probably not create our own parameter here auth_provider but rather use the existing definition of the sse or streamablehttp client which already take a auth parameter in the serverparams argument. Then we can just simply pass it through to the client.

Is there any limitations I am not seeing from the approach above?

@grll
Copy link
Owner

grll commented Sep 17, 2025

Maybe we should start in a first PR by introducing the passthrough auth parameter only and test it in a simple way as in:

https://github.com/modelcontextprotocol/python-sdk/blob/41184ba6c8b11c6f566445f10272ad541f6c0636/examples/snippets/clients/oauth_client.py

"""
Before running, specify running MCP RS server URL.
To spin up RS server locally, see
    examples/servers/simple-auth/README.md

cd to the `examples/snippets` directory and run:
    uv run oauth-client
"""

import asyncio
from urllib.parse import parse_qs, urlparse

from pydantic import AnyUrl

from mcp import ClientSession
from mcp.client.auth import OAuthClientProvider, TokenStorage
from mcp.client.streamable_http import streamablehttp_client
from mcp.shared.auth import OAuthClientInformationFull, OAuthClientMetadata, OAuthToken


class InMemoryTokenStorage(TokenStorage):
    """Demo In-memory token storage implementation."""

    def __init__(self):
        self.tokens: OAuthToken | None = None
        self.client_info: OAuthClientInformationFull | None = None

    async def get_tokens(self) -> OAuthToken | None:
        """Get stored tokens."""
        return self.tokens

    async def set_tokens(self, tokens: OAuthToken) -> None:
        """Store tokens."""
        self.tokens = tokens

    async def get_client_info(self) -> OAuthClientInformationFull | None:
        """Get stored client information."""
        return self.client_info

    async def set_client_info(self, client_info: OAuthClientInformationFull) -> None:
        """Store client information."""
        self.client_info = client_info


async def handle_redirect(auth_url: str) -> None:
    print(f"Visit: {auth_url}")


async def handle_callback() -> tuple[str, str | None]:
    callback_url = input("Paste callback URL: ")
    params = parse_qs(urlparse(callback_url).query)
    return params["code"][0], params.get("state", [None])[0]


async def main():
    """Run the OAuth client example."""
    oauth_auth = OAuthClientProvider(
        server_url="http://localhost:8001",
        client_metadata=OAuthClientMetadata(
            client_name="Example MCP Client",
            redirect_uris=[AnyUrl("http://localhost:3000/callback")],
            grant_types=["authorization_code", "refresh_token"],
            response_types=["code"],
            scope="user",
        ),
        storage=InMemoryTokenStorage(),
        redirect_handler=handle_redirect,
        callback_handler=handle_callback,
    )

    async with streamablehttp_client("http://localhost:8001/mcp", auth=oauth_auth) as (read, write, _):
        async with ClientSession(read, write) as session:
            await session.initialize()

            tools = await session.list_tools()
            print(f"Available tools: {[tool.name for tool in tools.tools]}")

            resources = await session.list_resources()
            print(f"Available resources: {[r.uri for r in resources.resources]}")


def run():
    asyncio.run(main())


if __name__ == "__main__":
    run()

@grll
Copy link
Owner

grll commented Sep 17, 2025

After we can think of carefully adding providers but I am still wondering if it's really something of the responsibility of MCPAdapt to provide and maintain

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.

Support for OAuth Authenticated Remote Servers
4 participants