mirror of
https://github.com/mcp-use/mcp-use.git
synced 2026-09-14 18:06:01 +08:00
aca5f9c040
* feat: Add PRM in the metadata discovery * fix: fallback anyway to PRM * feat: Implement complete discovery logic * feat: Final implementation of CIMD * feat: Implement PKCE * feat: Add scope from WWW-Authenticate * feat: Add all URLs possible styles * feat: Update tests and example * docs: Update docs * fix: linting * docs: update API documentation * fix: correct typos in OAuth metadata and error messages - Updated the comment in ProtectedResourceMetadata to use the correct abbreviation (PRM). - Fixed a typo in the OAuthAuthenticationError message regarding code challenge support. - Corrected the comment for legacy client paths to use "pre-registered" instead of "pre-reggistered". * feat: restructure authentication documentation and add new methods - Updated the authentication section in to group authentication methods under a new Authentication category with an icon. - Removed the old file and replaced it with new documentation files for Bearer Token, Custom Auth, and OAuth 2.1. - Introduced an overview page for authentication methods, detailing usage and configuration for each method. - Enhanced security best practices and examples for each authentication type to improve clarity and usability. * refactor: improve debug logging for OAuth metadata discovery - Updated debug messages to provide clearer context by specifying the issuer or host being queried during OAuth and OpenID Connect metadata discovery. - Enhanced error logging to remove redundant information, focusing on the failure reason without repeating the URL. * fix: correct abbreviation in ProtectedResourceMetadata documentation - Updated the abbreviation from PRC to PRM in the ProtectedResourceMetadata section to ensure accuracy in the documentation. * fix(oauth): correct RFC 8414 well-known URL construction for path-based issuers The OAuth metadata discovery was incorrectly constructing .well-known URLs by appending to the issuer instead of inserting between host and path. RFC 8414 specifies that for an issuer like https://github.com/login/oauth, the .well-known URL should be: https://github.com/.well-known/oauth-authorization-server/login/oauth We were incorrectly building: https://github.com/login/oauth/.well-known/oauth-authorization-server This fix: - Implements correct RFC 8414 URL construction (insert between host/path) - Supports OpenID Connect style as fallback (append to issuer) - Removes GitHub-specific hardcode since discovery now works correctly - Adds documentation comments explaining the two standards Ref: https://www.rfc-editor.org/rfc/rfc8414.html#section-3 * chore: oauth examples * docs: enhance OAuth endpoint discovery documentation - Updated the OAuth documentation to clarify how `mcp-use` discovers OAuth endpoints using both OAuth 2.0 and OpenID Connect standards. - Added a table detailing the URL construction methods for each standard. - Included examples for better understanding and a note on fallback behavior when the issuer has no path. * response to bs codeql * fix: update API URL Updated the GitHub API URL in the configuration to the correct endpoint for OAuth operations. --------- Co-authored-by: Enrico Toniato <2827496+tonxxd@users.noreply.github.com> Co-authored-by: pietrozullo <pietro.zullo@gmail.com>
202 lines
8.1 KiB
Python
202 lines
8.1 KiB
Python
import token
|
|
from pathlib import Path
|
|
from unittest.mock import AsyncMock, MagicMock, patch
|
|
|
|
import pytest
|
|
from httpx import Auth, Request
|
|
|
|
from mcp_use import MCPClient, set_debug
|
|
from mcp_use.client.auth.bearer import BearerAuth
|
|
|
|
set_debug(2)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_bearer_auth(auth_server):
|
|
"""Test Bearer token authentication."""
|
|
config = {"mcpServers": {"AuthServer": {"url": f"{auth_server}/mcp", "auth": "valid_token"}}}
|
|
client = MCPClient(config)
|
|
try:
|
|
await client.create_all_sessions()
|
|
session = client.get_session("AuthServer")
|
|
|
|
assert session.connector._auth is not None
|
|
|
|
# Test that we can call protected tools
|
|
result = await session.call_tool(name="protected_tool", arguments={})
|
|
assert result.content[0].text == "Authenticated access granted!"
|
|
|
|
# Test that we can call regular tools
|
|
result = await session.call_tool(name="add", arguments={"a": 5, "b": 3})
|
|
assert result.content[0].text == "8"
|
|
finally:
|
|
await client.close_all_sessions()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@patch("secrets.token_urlsafe")
|
|
@patch("webbrowser.open")
|
|
@patch("mcp_use.client.auth.oauth.OAuthCallbackServer")
|
|
async def test_oauth_provider(mock_callback_server_class, mock_webbrowser_open, mock_token_urlsafe, auth_server):
|
|
"""Test OAuth with pre-configured provider metadata."""
|
|
await clean_token()
|
|
|
|
# Mock the callback server
|
|
mock_callback_server = AsyncMock()
|
|
mock_callback_server.start.return_value = "http://127.0.0.1:8080/callback"
|
|
mock_callback_server.wait_for_code.return_value = MagicMock(
|
|
code="test_auth_code_12345", state="test_state_67890", error=None, error_description=None
|
|
)
|
|
mock_callback_server_class.return_value = mock_callback_server
|
|
mock_webbrowser_open.return_value = None
|
|
|
|
# Avoid CSRF error during testing
|
|
mock_token_urlsafe.return_value = "test_state_67890"
|
|
|
|
config = {
|
|
"mcpServers": {
|
|
"AuthServer": {
|
|
"url": f"{auth_server}/mcp",
|
|
"auth": {
|
|
"oauth_provider": {
|
|
"id": "auth_server",
|
|
"display_name": "AuthServer",
|
|
"metadata": {
|
|
"issuer": "http://127.0.0.1:8081",
|
|
"authorization_endpoint": "http://127.0.0.1:8081/oauth/authorize",
|
|
"token_endpoint": "http://127.0.0.1:8081/oauth/token",
|
|
"registration_endpoint": "http://127.0.0.1:8081/oauth/register",
|
|
"code_challenge_methods_supported": ["S256"],
|
|
},
|
|
}
|
|
},
|
|
}
|
|
}
|
|
}
|
|
client = MCPClient(config)
|
|
try:
|
|
await client.create_all_sessions()
|
|
session = client.get_session("AuthServer")
|
|
|
|
# Verify OAuth metadata was discovered
|
|
assert session.connector._oauth is not None, "OAuth should be initialized"
|
|
assert session.connector._oauth._metadata is not None, "OAuth metadata should be discovered"
|
|
assert str(session.connector._oauth._metadata.issuer) == "http://127.0.0.1:8081/"
|
|
assert str(session.connector._oauth._metadata.authorization_endpoint) == "http://127.0.0.1:8081/oauth/authorize"
|
|
assert str(session.connector._oauth._metadata.registration_endpoint) == "http://127.0.0.1:8081/oauth/register"
|
|
assert str(session.connector._oauth._metadata.token_endpoint) == "http://127.0.0.1:8081/oauth/token"
|
|
|
|
mock_webbrowser_open.assert_called_once()
|
|
mock_callback_server_class.assert_called_once()
|
|
mock_callback_server.start.assert_called_once()
|
|
mock_callback_server.wait_for_code.assert_called_once()
|
|
|
|
assert session.connector._auth is not None
|
|
|
|
# Test that we can call protected tools
|
|
result = await session.call_tool(name="protected_tool", arguments={})
|
|
assert result.content[0].text == "Authenticated access granted!"
|
|
|
|
# Test that we can call regular tools
|
|
result = await session.call_tool(name="add", arguments={"a": 5, "b": 3})
|
|
assert result.content[0].text == "8"
|
|
finally:
|
|
await client.close_all_sessions()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_custom_client(auth_server):
|
|
"Test that custom httpx.Auth objects works in auth field."
|
|
|
|
custom_auth = BearerAuth(token="valid_token")
|
|
config = {"mcpServers": {"AuthServer": {"url": f"{auth_server}/mcp", "auth": custom_auth}}}
|
|
|
|
client = MCPClient(config)
|
|
try:
|
|
await client.create_all_sessions()
|
|
session = client.get_session("AuthServer")
|
|
|
|
# Verify the custom BearerAuth is being used
|
|
assert session.connector._auth == custom_auth
|
|
|
|
# Test that we can call protected tools
|
|
result = await session.call_tool(name="protected_tool", arguments={})
|
|
assert result.content[0].text == "Authenticated access granted!"
|
|
|
|
# Test that we can call regular tools
|
|
result = await session.call_tool(name="add", arguments={"a": 5, "b": 3})
|
|
assert result.content[0].text == "8"
|
|
|
|
finally:
|
|
await client.close_all_sessions()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@patch("secrets.token_urlsafe")
|
|
@patch("webbrowser.open")
|
|
@patch("mcp_use.client.auth.oauth.OAuthCallbackServer")
|
|
async def test_oauth_complete_flow(mock_callback_server_class, mock_webbrowser_open, mock_token_urlsafe, auth_server):
|
|
"""Test OAuth complete flow, with metadata discovery, DCR and auth token."""
|
|
await clean_token()
|
|
|
|
# Mock the callback server
|
|
mock_callback_server = AsyncMock()
|
|
mock_callback_server.start.return_value = "http://127.0.0.1:8080/callback"
|
|
mock_callback_server.wait_for_code.return_value = MagicMock(
|
|
code="test_auth_code_12345", state="test_state_67890", error=None, error_description=None
|
|
)
|
|
mock_callback_server_class.return_value = mock_callback_server
|
|
mock_webbrowser_open.return_value = None
|
|
|
|
# Avoid CSRF error during testing
|
|
mock_token_urlsafe.return_value = "test_state_67890"
|
|
|
|
config = {
|
|
"mcpServers": {
|
|
"AuthServer": {
|
|
"url": f"{auth_server}/mcp",
|
|
}
|
|
}
|
|
}
|
|
client = MCPClient(config)
|
|
try:
|
|
await client.create_all_sessions()
|
|
session = client.get_session("AuthServer")
|
|
|
|
# Verify OAuth metadata was discovered
|
|
assert session.connector._oauth is not None, "OAuth should be initialized"
|
|
assert session.connector._oauth._metadata is not None, "OAuth metadata should be discovered"
|
|
assert str(session.connector._oauth._metadata.issuer) == "http://127.0.0.1:8081/"
|
|
assert str(session.connector._oauth._metadata.authorization_endpoint) == "http://127.0.0.1:8081/oauth/authorize"
|
|
assert str(session.connector._oauth._metadata.registration_endpoint) == "http://127.0.0.1:8081/oauth/register"
|
|
assert str(session.connector._oauth._metadata.token_endpoint) == "http://127.0.0.1:8081/oauth/token"
|
|
|
|
mock_webbrowser_open.assert_called_once()
|
|
mock_callback_server_class.assert_called_once()
|
|
mock_callback_server.start.assert_called_once()
|
|
mock_callback_server.wait_for_code.assert_called_once()
|
|
|
|
assert session.connector._auth is not None
|
|
|
|
# Test that we can call protected tools
|
|
result = await session.call_tool(name="protected_tool", arguments={})
|
|
assert result.content[0].text == "Authenticated access granted!"
|
|
|
|
# Test that we can call regular tools
|
|
result = await session.call_tool(name="add", arguments={"a": 5, "b": 3})
|
|
assert result.content[0].text == "8"
|
|
finally:
|
|
await client.close_all_sessions()
|
|
|
|
|
|
async def clean_token():
|
|
# Clear any existing tokens for this server before the test
|
|
token_dir = Path.home() / ".mcp_use" / "tokens"
|
|
if token_dir.exists():
|
|
for token_file in token_dir.glob("*127.0.0.1:8081__mcp.json"):
|
|
# Clear the file
|
|
token_file.unlink()
|
|
registrations_dir = token_dir / "registrations"
|
|
for registration_file in registrations_dir.glob("*127.0.0.1:8081__mcp_registration.json"):
|
|
registration_file.unlink()
|