Jeremiah Lowin commited on
Commit
7a8d3bf
·
2 Parent(s): 77b11ea2a723d9

Merge branch 'protocol-update' of https://github.com/jlowin/fastmcp into protocol-update

Browse files
src/fastmcp/server/auth/providers/bearer.py CHANGED
@@ -385,6 +385,21 @@ class BearerAuthProvider(OAuthProvider):
385
  return scope_claim
386
  return []
387
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
388
  # --- Unused OAuth server methods ---
389
  async def get_client(self, client_id: str) -> OAuthClientInformationFull | None:
390
  raise NotImplementedError("Client management not supported")
 
385
  return scope_claim
386
  return []
387
 
388
+ async def verify_token(self, token: str) -> AccessToken | None:
389
+ """
390
+ Verify a bearer token and return access info if valid.
391
+
392
+ This method implements the TokenVerifier protocol by delegating
393
+ to our existing load_access_token method.
394
+
395
+ Args:
396
+ token: The JWT token string to validate
397
+
398
+ Returns:
399
+ AccessToken object if valid, None if invalid or expired
400
+ """
401
+ return await self.load_access_token(token)
402
+
403
  # --- Unused OAuth server methods ---
404
  async def get_client(self, client_id: str) -> OAuthClientInformationFull | None:
405
  raise NotImplementedError("Client management not supported")
src/fastmcp/server/auth/providers/in_memory.py CHANGED
@@ -271,6 +271,21 @@ class InMemoryOAuthProvider(OAuthProvider):
271
  return token_obj
272
  return None
273
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
274
  def _revoke_internal(
275
  self, access_token_str: str | None = None, refresh_token_str: str | None = None
276
  ):
 
271
  return token_obj
272
  return None
273
 
274
+ async def verify_token(self, token: str) -> AccessToken | None:
275
+ """
276
+ Verify a bearer token and return access info if valid.
277
+
278
+ This method implements the TokenVerifier protocol by delegating
279
+ to our existing load_access_token method.
280
+
281
+ Args:
282
+ token: The token string to validate
283
+
284
+ Returns:
285
+ AccessToken object if valid, None if invalid or expired
286
+ """
287
+ return await self.load_access_token(token)
288
+
289
  def _revoke_internal(
290
  self, access_token_str: str | None = None, refresh_token_str: str | None = None
291
  ):
src/fastmcp/server/http.py CHANGED
@@ -87,7 +87,7 @@ def setup_auth_middleware_and_routes(
87
  middleware = [
88
  Middleware(
89
  AuthenticationMiddleware,
90
- backend=BearerAuthBackend(provider=auth),
91
  ),
92
  Middleware(AuthContextMiddleware),
93
  ]
 
87
  middleware = [
88
  Middleware(
89
  AuthenticationMiddleware,
90
+ backend=BearerAuthBackend(auth),
91
  ),
92
  Middleware(AuthContextMiddleware),
93
  ]
tests/auth/providers/test_token_verifier.py ADDED
@@ -0,0 +1,179 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Tests for TokenVerifier protocol implementation in auth providers."""
2
+
3
+ import pytest
4
+ from mcp.server.auth.provider import AccessToken
5
+
6
+ from fastmcp.server.auth.providers.bearer import BearerAuthProvider, RSAKeyPair
7
+ from fastmcp.server.auth.providers.in_memory import InMemoryOAuthProvider
8
+
9
+
10
+ class TestBearerAuthProviderTokenVerifier:
11
+ """Test that BearerAuthProvider implements TokenVerifier protocol correctly."""
12
+
13
+ @pytest.fixture
14
+ def rsa_key_pair(self) -> RSAKeyPair:
15
+ """Generate RSA key pair for testing."""
16
+ return RSAKeyPair.generate()
17
+
18
+ @pytest.fixture
19
+ def bearer_provider(self, rsa_key_pair: RSAKeyPair) -> BearerAuthProvider:
20
+ """Create BearerAuthProvider for testing."""
21
+ return BearerAuthProvider(
22
+ public_key=rsa_key_pair.public_key,
23
+ issuer="https://test.example.com",
24
+ audience="https://api.example.com",
25
+ )
26
+
27
+ @pytest.fixture
28
+ def valid_token(self, rsa_key_pair: RSAKeyPair) -> str:
29
+ """Create a valid test token."""
30
+ return rsa_key_pair.create_token(
31
+ subject="test-user",
32
+ issuer="https://test.example.com",
33
+ audience="https://api.example.com",
34
+ scopes=["read", "write"],
35
+ )
36
+
37
+ @pytest.fixture
38
+ def expired_token(self, rsa_key_pair: RSAKeyPair) -> str:
39
+ """Create an expired test token."""
40
+ return rsa_key_pair.create_token(
41
+ subject="test-user",
42
+ issuer="https://test.example.com",
43
+ audience="https://api.example.com",
44
+ expires_in_seconds=-3600, # Expired 1 hour ago
45
+ )
46
+
47
+ async def test_verify_token_with_valid_token(
48
+ self, bearer_provider: BearerAuthProvider, valid_token: str
49
+ ):
50
+ """Test that verify_token returns AccessToken for valid token."""
51
+ result = await bearer_provider.verify_token(valid_token)
52
+
53
+ assert result is not None
54
+ assert isinstance(result, AccessToken)
55
+ assert result.token == valid_token
56
+ assert result.client_id == "test-user"
57
+ assert "read" in result.scopes
58
+ assert "write" in result.scopes
59
+
60
+ async def test_verify_token_with_expired_token(
61
+ self, bearer_provider: BearerAuthProvider, expired_token: str
62
+ ):
63
+ """Test that verify_token returns None for expired token."""
64
+ result = await bearer_provider.verify_token(expired_token)
65
+ assert result is None
66
+
67
+ async def test_verify_token_with_invalid_token(
68
+ self, bearer_provider: BearerAuthProvider
69
+ ):
70
+ """Test that verify_token returns None for invalid token."""
71
+ result = await bearer_provider.verify_token("invalid.token.here")
72
+ assert result is None
73
+
74
+ async def test_verify_token_with_malformed_token(
75
+ self, bearer_provider: BearerAuthProvider
76
+ ):
77
+ """Test that verify_token returns None for malformed token."""
78
+ result = await bearer_provider.verify_token("not-a-jwt")
79
+ assert result is None
80
+
81
+ async def test_verify_token_delegation_to_load_access_token(
82
+ self, bearer_provider: BearerAuthProvider, valid_token: str
83
+ ):
84
+ """Test that verify_token delegates to load_access_token."""
85
+ # Both methods should return the same result
86
+ verify_result = await bearer_provider.verify_token(valid_token)
87
+ load_result = await bearer_provider.load_access_token(valid_token)
88
+
89
+ assert verify_result == load_result
90
+ if verify_result is not None and load_result is not None:
91
+ assert verify_result.token == load_result.token
92
+ assert verify_result.client_id == load_result.client_id
93
+ assert verify_result.scopes == load_result.scopes
94
+
95
+
96
+ class TestInMemoryOAuthProviderTokenVerifier:
97
+ """Test that InMemoryOAuthProvider implements TokenVerifier protocol correctly."""
98
+
99
+ @pytest.fixture
100
+ def in_memory_provider(self) -> InMemoryOAuthProvider:
101
+ """Create InMemoryOAuthProvider for testing."""
102
+ return InMemoryOAuthProvider(
103
+ issuer_url="https://test.example.com",
104
+ required_scopes=["user"],
105
+ )
106
+
107
+ async def test_verify_token_with_nonexistent_token(
108
+ self, in_memory_provider: InMemoryOAuthProvider
109
+ ):
110
+ """Test that verify_token returns None for nonexistent token."""
111
+ result = await in_memory_provider.verify_token("nonexistent-token")
112
+ assert result is None
113
+
114
+ async def test_verify_token_delegation_to_load_access_token(
115
+ self, in_memory_provider: InMemoryOAuthProvider
116
+ ):
117
+ """Test that verify_token delegates to load_access_token."""
118
+ # Create a test token in the provider's storage
119
+ test_token = "test-access-token"
120
+ test_access_token = AccessToken(
121
+ token=test_token,
122
+ client_id="test-client",
123
+ scopes=["user"],
124
+ expires_at=None, # No expiry
125
+ )
126
+ in_memory_provider.access_tokens[test_token] = test_access_token
127
+
128
+ # Both methods should return the same result
129
+ verify_result = await in_memory_provider.verify_token(test_token)
130
+ load_result = await in_memory_provider.load_access_token(test_token)
131
+
132
+ assert verify_result == load_result
133
+ assert verify_result is not None
134
+ assert verify_result.token == test_token
135
+ assert verify_result.client_id == "test-client"
136
+ assert verify_result.scopes == ["user"]
137
+
138
+ async def test_verify_token_with_expired_token(
139
+ self, in_memory_provider: InMemoryOAuthProvider
140
+ ):
141
+ """Test that verify_token returns None for expired token."""
142
+ import time
143
+
144
+ # Create an expired token
145
+ expired_token = "expired-token"
146
+ expired_access_token = AccessToken(
147
+ token=expired_token,
148
+ client_id="test-client",
149
+ scopes=["user"],
150
+ expires_at=int(time.time()) - 3600, # Expired 1 hour ago
151
+ )
152
+ in_memory_provider.access_tokens[expired_token] = expired_access_token
153
+
154
+ result = await in_memory_provider.verify_token(expired_token)
155
+ assert result is None
156
+
157
+ # Token should be cleaned up from storage
158
+ assert expired_token not in in_memory_provider.access_tokens
159
+
160
+
161
+ class TestTokenVerifierProtocolCompliance:
162
+ """Test that our providers properly implement the TokenVerifier protocol."""
163
+
164
+ async def test_bearer_provider_implements_protocol(self):
165
+ """Test that BearerAuthProvider can be used as TokenVerifier."""
166
+ key_pair = RSAKeyPair.generate()
167
+ provider = BearerAuthProvider(public_key=key_pair.public_key)
168
+
169
+ # Should have the required method for TokenVerifier protocol
170
+ assert hasattr(provider, "verify_token")
171
+ assert callable(provider.verify_token)
172
+
173
+ async def test_in_memory_provider_implements_protocol(self):
174
+ """Test that InMemoryOAuthProvider can be used as TokenVerifier."""
175
+ provider = InMemoryOAuthProvider()
176
+
177
+ # Should have the required method for TokenVerifier protocol
178
+ assert hasattr(provider, "verify_token")
179
+ assert callable(provider.verify_token)
tests/server/http/test_auth_setup.py ADDED
@@ -0,0 +1,187 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Tests for authentication setup in HTTP apps."""
2
+
3
+ import pytest
4
+ from mcp.server.auth.middleware.bearer_auth import BearerAuthBackend
5
+ from mcp.server.auth.provider import AccessToken
6
+ from starlette.middleware import Middleware
7
+ from starlette.middleware.authentication import AuthenticationMiddleware
8
+
9
+ from fastmcp.server.auth.providers.bearer import BearerAuthProvider, RSAKeyPair
10
+ from fastmcp.server.auth.providers.in_memory import InMemoryOAuthProvider
11
+ from fastmcp.server.http import setup_auth_middleware_and_routes
12
+
13
+
14
+ class TestSetupAuthMiddlewareAndRoutes:
15
+ """Test setup_auth_middleware_and_routes with TokenVerifier providers."""
16
+
17
+ @pytest.fixture
18
+ def bearer_provider(self) -> BearerAuthProvider:
19
+ """Create BearerAuthProvider for testing."""
20
+ key_pair = RSAKeyPair.generate()
21
+ return BearerAuthProvider(
22
+ public_key=key_pair.public_key,
23
+ issuer="https://test.example.com",
24
+ audience="https://api.example.com",
25
+ required_scopes=["read", "write"],
26
+ )
27
+
28
+ @pytest.fixture
29
+ def in_memory_provider(self) -> InMemoryOAuthProvider:
30
+ """Create InMemoryOAuthProvider for testing."""
31
+ return InMemoryOAuthProvider(
32
+ issuer_url="https://test.example.com",
33
+ required_scopes=["user"],
34
+ )
35
+
36
+ def test_setup_with_bearer_provider(self, bearer_provider: BearerAuthProvider):
37
+ """Test that setup works with BearerAuthProvider as TokenVerifier."""
38
+ middleware, auth_routes, required_scopes = setup_auth_middleware_and_routes(
39
+ bearer_provider
40
+ )
41
+
42
+ # Should return middleware list
43
+ assert isinstance(middleware, list)
44
+ assert len(middleware) == 2 # AuthenticationMiddleware + AuthContextMiddleware
45
+
46
+ # First middleware should be AuthenticationMiddleware with BearerAuthBackend
47
+ auth_middleware = middleware[0]
48
+ assert isinstance(auth_middleware, Middleware)
49
+ assert auth_middleware.cls == AuthenticationMiddleware
50
+ assert "backend" in auth_middleware.kwargs
51
+
52
+ backend = auth_middleware.kwargs["backend"]
53
+ assert isinstance(backend, BearerAuthBackend)
54
+ assert backend.token_verifier is bearer_provider # type: ignore[attr-defined]
55
+
56
+ # Should return auth routes
57
+ assert isinstance(auth_routes, list)
58
+ assert len(auth_routes) > 0 # Should have OAuth routes
59
+
60
+ # Should return required scopes
61
+ assert required_scopes == ["read", "write"]
62
+
63
+ def test_setup_with_in_memory_provider(
64
+ self, in_memory_provider: InMemoryOAuthProvider
65
+ ):
66
+ """Test that setup works with InMemoryOAuthProvider as TokenVerifier."""
67
+ middleware, auth_routes, required_scopes = setup_auth_middleware_and_routes(
68
+ in_memory_provider
69
+ )
70
+
71
+ # Should return middleware list
72
+ assert isinstance(middleware, list)
73
+ assert len(middleware) == 2
74
+
75
+ # Backend should use the provider as token verifier
76
+ auth_middleware = middleware[0]
77
+ backend = auth_middleware.kwargs["backend"]
78
+ assert isinstance(backend, BearerAuthBackend)
79
+ assert backend.token_verifier is in_memory_provider # type: ignore[attr-defined]
80
+
81
+ # Should return required scopes
82
+ assert required_scopes == ["user"]
83
+
84
+ def test_setup_preserves_provider_functionality(
85
+ self, bearer_provider: BearerAuthProvider
86
+ ):
87
+ """Test that setup doesn't break the provider's functionality."""
88
+ # Setup should not modify the provider
89
+ original_issuer = bearer_provider.issuer
90
+ original_scopes = bearer_provider.required_scopes
91
+
92
+ middleware, auth_routes, required_scopes = setup_auth_middleware_and_routes(
93
+ bearer_provider
94
+ )
95
+
96
+ # Provider should be unchanged
97
+ assert bearer_provider.issuer == original_issuer
98
+ assert bearer_provider.required_scopes == original_scopes
99
+
100
+ # Provider should still work as TokenVerifier
101
+ assert hasattr(bearer_provider, "verify_token")
102
+ assert callable(bearer_provider.verify_token)
103
+
104
+
105
+ class MockOAuthProvider:
106
+ """Mock OAuth provider that implements TokenVerifier."""
107
+
108
+ def __init__(self, required_scopes=None, issuer_url="http://localhost:8000"):
109
+ from pydantic import AnyHttpUrl
110
+
111
+ from fastmcp.server.auth.auth import (
112
+ ClientRegistrationOptions,
113
+ RevocationOptions,
114
+ )
115
+
116
+ self.required_scopes = required_scopes or []
117
+ self.issuer_url = AnyHttpUrl(issuer_url)
118
+ self.service_documentation_url = None
119
+ self.client_registration_options = ClientRegistrationOptions(enabled=False)
120
+ self.revocation_options = RevocationOptions(enabled=False)
121
+
122
+ async def verify_token(self, token: str) -> AccessToken | None:
123
+ """Mock verify_token implementation."""
124
+ if token == "valid-token":
125
+ return AccessToken(
126
+ token=token,
127
+ client_id="mock-client",
128
+ scopes=self.required_scopes,
129
+ expires_at=None,
130
+ )
131
+ return None
132
+
133
+
134
+ class TestSetupWithMockProvider:
135
+ """Test setup function with mock provider."""
136
+
137
+ def test_setup_with_mock_token_verifier(self):
138
+ """Test that setup works with any TokenVerifier implementation."""
139
+ mock_provider = MockOAuthProvider(required_scopes=["mock-scope"])
140
+
141
+ middleware, auth_routes, required_scopes = setup_auth_middleware_and_routes(
142
+ mock_provider # type: ignore[arg-type]
143
+ )
144
+
145
+ # Should work with any TokenVerifier
146
+ assert len(middleware) == 2
147
+ auth_middleware = middleware[0]
148
+ backend = auth_middleware.kwargs["backend"]
149
+ assert isinstance(backend, BearerAuthBackend)
150
+ assert backend.token_verifier is mock_provider # type: ignore[attr-defined]
151
+
152
+ assert required_scopes == ["mock-scope"]
153
+
154
+ async def test_setup_middleware_can_authenticate(self):
155
+ """Test that the setup middleware can actually authenticate requests."""
156
+ mock_provider = MockOAuthProvider()
157
+
158
+ middleware, _, _ = setup_auth_middleware_and_routes(mock_provider) # type: ignore[arg-type]
159
+
160
+ # Extract the BearerAuthBackend
161
+ auth_middleware = middleware[0]
162
+ backend = auth_middleware.kwargs["backend"]
163
+
164
+ # Test authentication with valid token
165
+ from starlette.requests import HTTPConnection
166
+
167
+ scope = {
168
+ "type": "http",
169
+ "headers": [(b"authorization", b"Bearer valid-token")],
170
+ }
171
+ conn = HTTPConnection(scope)
172
+
173
+ result = await backend.authenticate(conn) # type: ignore[attr-defined]
174
+ assert result is not None
175
+
176
+ credentials, user = result
177
+ assert user.username == "mock-client"
178
+
179
+ # Test authentication with invalid token
180
+ scope = {
181
+ "type": "http",
182
+ "headers": [(b"authorization", b"Bearer invalid-token")],
183
+ }
184
+ conn = HTTPConnection(scope)
185
+
186
+ result = await backend.authenticate(conn) # type: ignore[attr-defined]
187
+ assert result is None
tests/server/http/test_bearer_auth_backend.py ADDED
@@ -0,0 +1,178 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Tests for BearerAuthBackend integration with TokenVerifier."""
2
+
3
+ import pytest
4
+ from mcp.server.auth.middleware.bearer_auth import BearerAuthBackend
5
+ from mcp.server.auth.provider import AccessToken
6
+ from starlette.requests import HTTPConnection
7
+
8
+ from fastmcp.server.auth.providers.bearer import BearerAuthProvider, RSAKeyPair
9
+
10
+
11
+ class TestBearerAuthBackendTokenVerifierIntegration:
12
+ """Test BearerAuthBackend works with TokenVerifier protocol."""
13
+
14
+ @pytest.fixture
15
+ def rsa_key_pair(self) -> RSAKeyPair:
16
+ """Generate RSA key pair for testing."""
17
+ return RSAKeyPair.generate()
18
+
19
+ @pytest.fixture
20
+ def bearer_provider(self, rsa_key_pair: RSAKeyPair) -> BearerAuthProvider:
21
+ """Create BearerAuthProvider for testing."""
22
+ return BearerAuthProvider(
23
+ public_key=rsa_key_pair.public_key,
24
+ issuer="https://test.example.com",
25
+ audience="https://api.example.com",
26
+ )
27
+
28
+ @pytest.fixture
29
+ def valid_token(self, rsa_key_pair: RSAKeyPair) -> str:
30
+ """Create a valid test token."""
31
+ return rsa_key_pair.create_token(
32
+ subject="test-user",
33
+ issuer="https://test.example.com",
34
+ audience="https://api.example.com",
35
+ scopes=["read", "write"],
36
+ )
37
+
38
+ def test_bearer_auth_backend_constructor_accepts_token_verifier(
39
+ self, bearer_provider: BearerAuthProvider
40
+ ):
41
+ """Test that BearerAuthBackend constructor accepts TokenVerifier."""
42
+ # This should not raise an error
43
+ backend = BearerAuthBackend(bearer_provider)
44
+ assert backend.token_verifier is bearer_provider # type: ignore[attr-defined]
45
+
46
+ async def test_bearer_auth_backend_authenticate_with_valid_token(
47
+ self, bearer_provider: BearerAuthProvider, valid_token: str
48
+ ):
49
+ """Test BearerAuthBackend authentication with valid token."""
50
+ backend = BearerAuthBackend(bearer_provider)
51
+
52
+ # Create mock HTTPConnection with Authorization header
53
+ scope = {
54
+ "type": "http",
55
+ "headers": [(b"authorization", f"Bearer {valid_token}".encode())],
56
+ }
57
+ conn = HTTPConnection(scope)
58
+
59
+ result = await backend.authenticate(conn)
60
+
61
+ assert result is not None
62
+ credentials, user = result
63
+ assert credentials.scopes == ["read", "write"]
64
+ assert user.username == "test-user"
65
+ assert hasattr(user, "access_token")
66
+ assert user.access_token.token == valid_token
67
+
68
+ async def test_bearer_auth_backend_authenticate_with_invalid_token(
69
+ self, bearer_provider: BearerAuthProvider
70
+ ):
71
+ """Test BearerAuthBackend authentication with invalid token."""
72
+ backend = BearerAuthBackend(bearer_provider)
73
+
74
+ # Create mock HTTPConnection with invalid Authorization header
75
+ scope = {
76
+ "type": "http",
77
+ "headers": [(b"authorization", b"Bearer invalid-token")],
78
+ }
79
+ conn = HTTPConnection(scope)
80
+
81
+ result = await backend.authenticate(conn)
82
+ assert result is None
83
+
84
+ async def test_bearer_auth_backend_authenticate_with_no_header(
85
+ self, bearer_provider: BearerAuthProvider
86
+ ):
87
+ """Test BearerAuthBackend authentication with no Authorization header."""
88
+ backend = BearerAuthBackend(bearer_provider)
89
+
90
+ # Create mock HTTPConnection without Authorization header
91
+ scope = {
92
+ "type": "http",
93
+ "headers": [],
94
+ }
95
+ conn = HTTPConnection(scope)
96
+
97
+ result = await backend.authenticate(conn)
98
+ assert result is None
99
+
100
+ async def test_bearer_auth_backend_authenticate_with_non_bearer_token(
101
+ self, bearer_provider: BearerAuthProvider
102
+ ):
103
+ """Test BearerAuthBackend authentication with non-Bearer token."""
104
+ backend = BearerAuthBackend(bearer_provider)
105
+
106
+ # Create mock HTTPConnection with Basic auth header
107
+ scope = {
108
+ "type": "http",
109
+ "headers": [(b"authorization", b"Basic dXNlcjpwYXNz")],
110
+ }
111
+ conn = HTTPConnection(scope)
112
+
113
+ result = await backend.authenticate(conn)
114
+ assert result is None
115
+
116
+
117
+ class MockTokenVerifier:
118
+ """Mock TokenVerifier for testing backend integration."""
119
+
120
+ def __init__(self, return_value: AccessToken | None = None):
121
+ self.return_value = return_value
122
+ self.verify_token_calls = []
123
+
124
+ async def verify_token(self, token: str) -> AccessToken | None:
125
+ """Mock verify_token method."""
126
+ self.verify_token_calls.append(token)
127
+ return self.return_value
128
+
129
+
130
+ class TestBearerAuthBackendWithMockVerifier:
131
+ """Test BearerAuthBackend with mock TokenVerifier."""
132
+
133
+ async def test_backend_calls_verify_token_method(self):
134
+ """Test that BearerAuthBackend calls verify_token on the verifier."""
135
+ mock_access_token = AccessToken(
136
+ token="test-token",
137
+ client_id="test-client",
138
+ scopes=["read"],
139
+ expires_at=None,
140
+ )
141
+ mock_verifier = MockTokenVerifier(return_value=mock_access_token)
142
+ backend = BearerAuthBackend(mock_verifier) # type: ignore[arg-type]
143
+
144
+ scope = {
145
+ "type": "http",
146
+ "headers": [(b"authorization", b"Bearer test-token")],
147
+ }
148
+ conn = HTTPConnection(scope)
149
+
150
+ result = await backend.authenticate(conn)
151
+
152
+ # Should have called verify_token with the token
153
+ assert mock_verifier.verify_token_calls == ["test-token"]
154
+
155
+ # Should return authentication result
156
+ assert result is not None
157
+ credentials, user = result
158
+ assert credentials.scopes == ["read"]
159
+ assert user.username == "test-client"
160
+
161
+ async def test_backend_handles_verify_token_none_result(self):
162
+ """Test that BearerAuthBackend handles None result from verify_token."""
163
+ mock_verifier = MockTokenVerifier(return_value=None)
164
+ backend = BearerAuthBackend(mock_verifier) # type: ignore[arg-type]
165
+
166
+ scope = {
167
+ "type": "http",
168
+ "headers": [(b"authorization", b"Bearer invalid-token")],
169
+ }
170
+ conn = HTTPConnection(scope)
171
+
172
+ result = await backend.authenticate(conn)
173
+
174
+ # Should have called verify_token
175
+ assert mock_verifier.verify_token_calls == ["invalid-token"]
176
+
177
+ # Should return None for authentication failure
178
+ assert result is None
uv.lock CHANGED
@@ -482,7 +482,7 @@ requires-dist = [
482
  { name = "authlib", specifier = ">=1.5.2" },
483
  { name = "exceptiongroup", specifier = ">=1.2.2" },
484
  { name = "httpx", specifier = ">=0.28.1" },
485
- { name = "mcp", git = "https://github.com/modelcontextprotocol/python-sdk.git?rev=main" },
486
  { name = "openapi-pydantic", specifier = ">=0.5.1" },
487
  { name = "python-dotenv", specifier = ">=1.1.0" },
488
  { name = "rich", specifier = ">=13.9.4" },
@@ -746,8 +746,8 @@ wheels = [
746
 
747
  [[package]]
748
  name = "mcp"
749
- version = "1.10.1.dev1+dced223"
750
- source = { git = "https://github.com/modelcontextprotocol/python-sdk.git?rev=main#dced22344d80d85884df9c26281fdfe0331e9c65" }
751
  dependencies = [
752
  { name = "anyio" },
753
  { name = "httpx" },
@@ -760,6 +760,10 @@ dependencies = [
760
  { name = "starlette" },
761
  { name = "uvicorn", marker = "sys_platform != 'emscripten'" },
762
  ]
 
 
 
 
763
 
764
  [[package]]
765
  name = "mdurl"
 
482
  { name = "authlib", specifier = ">=1.5.2" },
483
  { name = "exceptiongroup", specifier = ">=1.2.2" },
484
  { name = "httpx", specifier = ">=0.28.1" },
485
+ { name = "mcp", specifier = ">=1.10.0" },
486
  { name = "openapi-pydantic", specifier = ">=0.5.1" },
487
  { name = "python-dotenv", specifier = ">=1.1.0" },
488
  { name = "rich", specifier = ">=13.9.4" },
 
746
 
747
  [[package]]
748
  name = "mcp"
749
+ version = "1.10.0"
750
+ source = { registry = "https://pypi.org/simple" }
751
  dependencies = [
752
  { name = "anyio" },
753
  { name = "httpx" },
 
760
  { name = "starlette" },
761
  { name = "uvicorn", marker = "sys_platform != 'emscripten'" },
762
  ]
763
+ sdist = { url = "https://files.pythonhosted.org/packages/c8/1a/d90e42be23a7e6dd35c03e35c7c63fe1036f082d3bb88114b66bd0f2467e/mcp-1.10.0.tar.gz", hash = "sha256:91fb1623c3faf14577623d14755d3213db837c5da5dae85069e1b59124cbe0e9", size = 392961, upload-time = "2025-06-26T13:51:19.025Z" }
764
+ wheels = [
765
+ { url = "https://files.pythonhosted.org/packages/0f/52/e1c43c4b5153465fd5d3b4b41bf2d4c7731475e9f668f38d68f848c25c9a/mcp-1.10.0-py3-none-any.whl", hash = "sha256:925c45482d75b1b6f11febddf9736d55edf7739c7ea39b583309f6651cbc9e5c", size = 150894, upload-time = "2025-06-26T13:51:17.342Z" },
766
+ ]
767
 
768
  [[package]]
769
  name = "mdurl"